├── .gitignore ├── .ruby-version ├── .yardopts ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── golden ├── examples ├── images │ ├── Rakefile │ ├── ch1.md │ └── images │ │ └── image1.png ├── markdown-basic │ ├── Rakefile │ ├── empty.md │ ├── intro.md │ └── part1 │ │ └── ch1.md ├── metadata │ ├── README │ ├── Rakefile │ ├── intro.md │ └── part1 │ │ └── ch1.md ├── minimal │ ├── README │ ├── Rakefile │ ├── intro.md │ └── part1 │ │ └── ch1.md ├── orgmode-basic │ ├── Rakefile │ └── book.org ├── pandoc_epub │ ├── Rakefile │ └── ch1.md ├── source-listings │ ├── Rakefile │ ├── ch1.md │ └── ch2.md ├── structure │ ├── README │ ├── Rakefile │ ├── ch1.markdown │ ├── ch2.html │ ├── ch2.org │ ├── ch3-implicit.markdown │ ├── ch4-implicit.org │ ├── foreward.markdown │ ├── preface.markdown │ └── subdir │ │ └── ch5.markdown └── website │ ├── Rakefile │ ├── ch1.markdown │ └── ch2.markdown ├── fontforge └── convert.pe ├── lib ├── quarto.rb └── quarto │ ├── bower.rb │ ├── build.rb │ ├── bundle.rb │ ├── calibre_mobi.rb │ ├── doc_raptor.rb │ ├── epubcheck.rb │ ├── font.rb │ ├── git.rb │ ├── kindlegen.rb │ ├── markdown.rb │ ├── orgmode.rb │ ├── pandoc_epub.rb │ ├── path_helpers.rb │ ├── pdf_samples.rb │ ├── plugin.rb │ ├── prince.rb │ ├── site.rb │ ├── stylesheet.rb │ ├── stylesheet_set.rb │ ├── tasks.rb │ ├── template.rb │ ├── template_set.rb │ ├── uri_helpers.rb │ └── version.rb ├── quarto.gemspec ├── spec ├── env.rb ├── golden │ └── master │ │ ├── rake-codex-with-custom-metadata │ │ └── build │ │ │ └── codex.xhtml │ │ ├── rake-codex-with-minimal-config │ │ └── build │ │ │ └── codex.xhtml │ │ ├── rake-export-with-markdown-sources │ │ └── build │ │ │ └── exports │ │ │ ├── intro.html │ │ │ └── part1 │ │ │ └── ch1.html │ │ ├── rake-export-with-orgmode-sources │ │ └── build │ │ │ └── exports │ │ │ └── book.html │ │ ├── rake-highlight-highlights-source-listings │ │ └── build │ │ │ └── highlights │ │ │ ├── 3361c5f02e08bd44bde2d42633a2c9be201f7ec4.html │ │ │ └── b8f5d0e6fa84ab657a95f4e67d1093abcc9dd3df.html │ │ ├── rake-master-builds-a-master-file-and-links-in-images │ │ └── build │ │ │ └── master │ │ │ ├── images │ │ │ └── image1.png │ │ │ └── master.xhtml │ │ ├── rake-pandoc-epub-epub-generates-epub │ │ └── build │ │ │ └── deliverables │ │ │ └── untitled-book.epub.golden_child_unzip │ │ │ ├── META-INF │ │ │ └── container.xml │ │ │ ├── content.opf │ │ │ └── mimetype │ │ ├── rake-sections-with-orgmode-sources │ │ └── build │ │ │ └── sections │ │ │ └── book.xhtml │ │ ├── rake-signatures-with-markdown-sources │ │ └── build │ │ │ └── signatures │ │ │ ├── empty.xhtml │ │ │ ├── intro.xhtml │ │ │ └── part1 │ │ │ └── ch1.xhtml │ │ ├── rake-signatures-with-orgmode-sources │ │ └── build │ │ │ └── signatures │ │ │ └── book.xhtml │ │ ├── rake-site-build-builds-a-website │ │ └── build │ │ │ └── site │ │ │ ├── fascicles │ │ │ ├── 001-ch1.html │ │ │ └── 002-ch2.html │ │ │ └── index.html │ │ ├── rake-skeleton │ │ └── build │ │ │ ├── listings │ │ │ ├── 3361c5f02e08bd44bde2d42633a2c9be201f7ec4.rb │ │ │ └── b8f5d0e6fa84ab657a95f4e67d1093abcc9dd3df.c │ │ │ └── skeleton.xhtml │ │ └── rake-structure-generates-a-coherent-book-structure-from-heterogenous-inputs │ │ └── build │ │ ├── master │ │ └── master.xhtml │ │ └── structure.yaml ├── spec_helper.rb └── tasks │ ├── codex_spec.rb │ ├── export_spec.rb │ ├── highlight_spec.rb │ ├── master_spec.rb │ ├── pandoc_epub_spec.rb │ ├── signatures_spec.rb │ ├── site_spec.rb │ ├── skeleton_spec.rb │ └── structure_spec.rb └── templates ├── .bowerrc ├── bower.json.erb ├── site ├── _book_metadata.html.slim ├── _fascicle.html.slim ├── _layout.html.slim ├── _toc.html.slim └── index.html.slim └── stylesheets ├── base.scss ├── code.scss ├── epub2.scss ├── epub3.scss ├── pages.scss ├── pdf.scss └── toc.scss /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | /scratch* 19 | /vendor 20 | /.idea 21 | /spec/golden/actual 22 | /.golden_child/state.yaml 23 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.1.2 -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup markdown 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | ruby "2.1.2" 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in quarto.gemspec 6 | gemspec 7 | 8 | gem "golden_child", "~> 0.0.1", git: "avdi/golden_child", branch: "master" 9 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Avdi Grimm 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PullReview stats](https://www.pullreview.com/github/avdi/quarto/badges/master.svg?)](https://www.pullreview.com/github/avdi/quarto/reviews/master) 2 | 3 | # Quarto 4 | 5 | Yet another ebook generation toolchain, biased towards writing books about programming. 6 | 7 | About the name: "Quarto" is a bookbinding term, and this is my fourth attempt at a reusable ebook toolchain. 8 | 9 | ## Important Note 10 | 11 | Development on Quarto will necessarily occur in fits and starts, because I'll only be working on it actively while I'm writing a book. 12 | 13 | It is also very, very unsupported. Right now it exists to solve *my* problems... barely. If it solves your problems too that's fantastic, but I don't have time to help you get it working. available! 14 | 15 | ## Notable Features 16 | 17 | - Accept either Markdown or Org-Mode input files. 18 | - XHTML5 as a universal intermediate format. CSS as a universal styling format. No more maintaining parallel LaTeX or XSL-FO styles for the PDF target. 19 | - SCSS (SASS) support for CSS files. 20 | - PDF output via local PrinceXML **or** DocRaptor. 21 | - Gives DocRaptor a fully standalone source document, with fonts and images embedded using data URIs. There is no need to have the images or fonts be publicly accessible somewhere. 22 | - EPUB3 output via Pandoc. 23 | - EPUB3 font embedding. 24 | - Epubcheck can be incorporated into the production line for automatic validation of generated EPUBs. 25 | - Mobi (Kindle) output via Kindlegen. *Note:* according to the Kindlegen terms of service, the resulting output can only be sold in the Amazon store. 26 | - When producing EPUB, automatically converts unsupported font types to OpenType using FontForge. 27 | - Source code highlighting via Pygments, for maximum breadth of language support. 28 | - Optimized source code highlighting tracks individual listings by SHA1 and only highlights listings that have changed or are new. It also runs multiple highlighting processes in parallel. 29 | 30 | ## Requirements 31 | 32 | Quarto depends on several external programs which you will need to install before using it. Some of these are only required if you use the corresponding plugin. 33 | 34 | - Git 35 | - Pandoc 36 | - Pygments 37 | - xmllint 38 | - PrinceXML (the free trial version is fine) 39 | - xmlstarlet 40 | - FontForge 41 | 42 | ## Getting Started 43 | 44 | *Note*: Quarto is available as a Ruby gem, which you can install in the 45 | usual way (`gem install quarto`). But while the gem provides 46 | stability, it is laughably out of date. Indeed, installing the gem 47 | (instead of pointing to a GitHub repository) almost guarantees that 48 | these instructions will not work. 49 | 50 | 1. Create a new directory, in which your book project will reside: 51 | 52 | `mkdir mybook` 53 | 54 | 2. Inside of the directory, create a `Rakefile`, containing the 55 | following: 56 | 57 | ```ruby 58 | require 'quarto/tasks' 59 | ``` 60 | 61 | This `Rakefile` is what you will use to run Quarto's Rake tasks, 62 | which are what you will use to create and publish your book. 63 | 64 | 3. Now create a `Gemfile`. This will be used by 65 | [Bundler](ttp://bundler.io) to install the required Ruby gems, 66 | including Quarto. The `Gemfile` should look, at a minimum, like 67 | this: 68 | 69 | ```ruby 70 | gem 'rake' 71 | gem 'quarto', github: 'avdi/quarto' 72 | ``` 73 | 74 | Note that this is true if you want to be using the original, Avdi 75 | Grimm-authored version of Quarto. If, by contrast, you want fork 76 | Quarto into your own GitHub repository, then you will want to 77 | point to that. 78 | 79 | 4. Now run `bundle install`, which will create a `Gemfile.lock`. 80 | 81 | 5. Run `rake -T` to see the available tasks. 82 | 83 | 6. The task you care about is probably `rake deliverables`. (This is also the default.) 84 | 85 | ## Concepts 86 | 87 | Quarto is a set of Rake tasks backed up by a Ruby library, which in turn relies heavily on Nokogiri and a number of external tools. 88 | 89 | ### Flexibility 90 | 91 | Quarto doesn't (yet) introduce any revolutionary ideas to e-publishing. Instead, it ties familiar tools together in a way that lets you write the way you want to. 92 | 93 | There are a lot of tools that try to tie together an end-to-end publishing pipeline. But when you want to interpose your own processing in between steps, you're out of luck. The fact that Quarto is structured as a set of Rake tasks means that you can add your own dependencies, your own steps, or tack extra processing onto any of the existing steps just by adding to your project's `Rakefile`. 94 | 95 | ### Explorability 96 | 97 | There is a well-defined set of steps with documented inputs and output artifacts (see below). These artifacts of the build process are left behind in a `build` directory in your project root, so it's easy to understand what Quarto is doing at each step of the way. There's nothing hidden in anonymous temporary files. 98 | 99 | ### The best tools for the job 100 | 101 | Quarto tries to pick the best tools for each step in the book-building chain. So for instance, while some Markdown parsers support limited syntax highlighting of source code, Quarto instead uses Pygments to highlight code listings as a separate (and highly optimized) step. This ensures that high-quality highlighting is available for the widest possible variety of source code languages. 102 | 103 | ### Prefer the command line 104 | 105 | When there is an option to either perform an operation in pure Ruby and shell out to a command-line tool without too much added pain, Quarto prefers to shell out to the tool. This may seem counterintuitive, since it means more dependencies. The advantage is that since Rake echoes shell commands to the console, you can *see* exactly what Quarto is doing. You can even copy and paste the commands to try them yourself. 106 | 107 | In the future, I hope to make it so that even tasks that are implemented in pure Ruby can be easily invoked independently from the command line. The goal is to have the output of a Quarto run be a series of commands that you could run manually and get the same results. 108 | 109 | ### XHTML5 is king 110 | 111 | A central philosophy of Quarto is to do as much work as possible with XHTML5 files. All input formats (e.g. Markdown) are first converted to XHTML5 before any other work is done. Then various transformations occur. Finally, at the end of the line, an XHTML5 "master" file is converted to various deliverable formats such as PDF. The reason for this philosophy is simple: Nokogiri makes it really easy to perform arbitrary semantic transformations on XHTML documents, without a lot of tedious mucking about with text munging. The more of the work that is done on DOM object trees, the easier it is to do. 112 | 113 | XHTML5 is also sufficiently rich and expressive that most formats can be converted to it without losing information. And XHTML5 is at the heart of both EPUB3 and Kindle Format 8, the leading ebook publishing formats. 114 | 115 | ### The assembly line 116 | 117 | Quarto is a set of Rake tasks, so execution normally starts with a desired end product and works backwards through the dependency chain to figure out what needs to be done to produce that product. However, it's probably easier to understand everything Quarto does by viewing it as an assembly line starting with source files and ending with deliverables. Here are the steps along the way. 118 | 119 | Note that all files generated by Quarto are placed in a `build` subdirectory of your project's root. It will be created if needed. 120 | 121 | 1. **Source files**. These are manuscript files in supported source formats (currently Markdown and Org-Mode). They might be in the root of your project, or in subdirectories. 122 | 2. Source files are *exported* into **export files** in `build/export`. Export files are HTML, produced using whatever tool is appropriate for the input format. E.g. `pandoc` is used to export Markdown source files to HTML equivalents. 123 | 3. The source files are then normalized into XHTML **signature files** (in `build/signatures`). During this normalization process any idiosyncrasies in the HTML produced by the export tool are dealt with. 124 | 4. A **spine file** is generated. This XHTML file will be used to tie together all of the section files. The body of this file contains references to (but not the content of) all of the signature files. It also contains stylesheets and other metadata. 125 | 5. The spine file is then expanded into an XHTML **codex file**. This file contains the body content of all of the signature files concatenated together. Only body content is taken from the signature files, everything else is ignored. From this point forward, all operations will be done on monolithic files rather than on partial files corresponding to the original sources. 126 | 6. The codex file is searched for source code listings. Each listing is extracted out as text into a **listing file** (in `build/listings`). Listing files are named based on the SHA1 of the listing and its language, e.g. `build/listings/3361c5f02e08bd44bde2d42633a2c9be201f7ec4.rb`. Using the SHA1 in naming is an optimization which ensures that only changed code listings ever need to be re-highlighted (see the next step). During this step a **skeleton file** is also created. This XHTML file mirrors the codex file, except that all of the source code listings have been replaced with references to highlight files (see next step). 127 | 7. The next step is to perform source code highlighting on the listing files, using Pygments. This produces **highlight files**, which are HTML files in the `build/highlights` directory. They named based on the SHA1 of the corresponding code listing. 128 | 8. The skeleton file and the highlights file are then stitched back together into a **master file**. This XHTML file is the "gold standard" from which all deliverables will be generated. 129 | 9. Some end products, such as a generated web site, may need the book to be re-broken into individual files. Toward this end, the master file is split into **fascicle files**. ("Fascicle" is a term for an individual part of a book that has been printed as a serial.) A fascicle contains the body of one of the original section files, but with all the styling, metadata, source highlighting, etc. of a master file. A fascicle is a good candidate for generating standalone "sample chapters". 130 | 10. **Deliverable files** suitable for distributing to end-users, such as PDF or Epub files, are produced using the master file. The plugins that handle production of deliverables may create various other intermediate files during this process. 131 | 132 | ## Detailed Usage 133 | 134 | ### Configuration 135 | 136 | Quarto can be configured by requiring `quarto` instead of `quarto/tasks`, and calling `Quarto.configure`. Here is the configuration for my book "Confident Ruby": 137 | 138 | ```ruby 139 | require 'quarto' 140 | 141 | Quarto.configure do |config| 142 | config.author = "Avdi Grimm" 143 | config.title = "Confident Ruby" 144 | 145 | config.use :git 146 | config.use :orgmode # if you want to use org-mode 147 | config.use :markdown # if you want to use markdown 148 | config.use :doc_raptor 149 | config.use :pandoc_epub 150 | config.use :epubcheck 151 | config.use :kindlegen 152 | config.use :bundle 153 | config.source_files = ["confident-ruby.org"] 154 | config.bitmap_cover_image = "images/cover-large.png" 155 | config.vector_cover_image = "images/cover.svg" 156 | config.stylesheets.cover_color = "#fff4cd" 157 | config.stylesheets.heading_font = '"PT Sans", sans-serif' 158 | config.stylesheets.font = '"PT Serif", serif' 159 | config.add_font("PT Sans", file: "fonts/PT_Sans-Web-Regular.ttf") 160 | config.add_font( 161 | "PT Sans", 162 | weight: "bold", 163 | file: "fonts/PT_Sans-Web-Bold.ttf") 164 | config.add_font( 165 | "PT Sans", 166 | style: "italic", 167 | file: "fonts/PT_Sans-Web-Italic.ttf") 168 | config.add_font( 169 | "PT Sans", 170 | weight: "bold", 171 | style: "italic", 172 | file: "fonts/PT_Sans-Web-BoldItalic.ttf") 173 | config.add_font("PT Serif", file: "fonts/PT_Serif-Web-Regular.ttf") 174 | config.add_font( 175 | "PT Serif", 176 | weight: "bold", 177 | file: "fonts/PT_Serif-Web-Bold.ttf") 178 | config.add_font( 179 | "PT Serif", 180 | style: "italic", 181 | file: "fonts/PT_Serif-Web-Italic.ttf") 182 | config.add_font( 183 | "PT Serif", 184 | weight: "bold", 185 | style: "italic", 186 | file: "fonts/PT_Serif-Web-BoldItalic.ttf") 187 | config.add_font("Source Code Pro", file: "fonts/SourceCodePro-Regular.otf") 188 | config.add_font( 189 | "Source Code Pro", 190 | weight: "bold", 191 | file: "fonts/SourceCodePro-Bold.otf") 192 | end 193 | ``` 194 | 195 | ### Explicitly setting source files 196 | 197 | By default Quarto seeks out source files with extensions it knows about. Alternatively, you can explicitly define the list of source files to use, and the order in which to use them. 198 | 199 | ```ruby 200 | Quarto.configure do |config| 201 | config.source_files << [ 202 | "ch1.md", 203 | "subdir/ch3.org" 204 | ] 205 | end 206 | ``` 207 | 208 | ### Excluding files 209 | 210 | By default Quarto looks for any source files with extensions it knows (such as `.md`) and includes them in the build. There are some exceptions to this however, and some ways to influence which source files it considers for inclusion. 211 | 212 | First of all, the `build` directory will always be excluded from the search. So will any `.git` directory. 213 | 214 | If the project is under Git control and you use the `:git` plugin, any files ignored by Git (via `.gitignore`) will be ignored by Quarto. 215 | 216 | Finally, you can add exclusion patterns in the Quarto configuration. 217 | 218 | ```ruby 219 | Quarto.configure do |config| 220 | config.exclude_sources("~*") 221 | end 222 | ``` 223 | 224 | Exclusion patterns can be shell glob strings or regular expressions (anything supported by Rake [`FileList#exclude`](http://rake.rubyforge.org/Rake/FileList.html#method-i-exclude)). 225 | 226 | ### Enabling optional functionality 227 | 228 | ```ruby 229 | Quarto.configure do |config| 230 | config.use :orgmode 231 | end 232 | ``` 233 | 234 | ## Building plugins 235 | 236 | Quarto offers the ability to alter the assembly line to generate alternative output. 237 | 238 | Start by creating a class for your plugin which inherits from `Quarto::Plugin`. Quarto will expect that your plugin has features for different aspects of the assembly line. 239 | 240 | You have the option to provide custom behavior for the following methods: 241 | 242 | - `enhance_build(build_object)`: This method accepts the Quarto build object and will be used to affect the build process. See example below. 243 | - `finalize_build(build_object)`: This method also accepts the Quarto build object and hooks into the process after the build object has been initialized and processed through all plugins and their `enhance_build` methods. 244 | - `define_tasks`: Your plugin has access to the Rake::DSL library for creating command line rake tasks. Use this method to define tasks that can be used to manipulate and output content necessary for your plugin to function. Rake allows you to access existing rake tasks so you may compose your part of the assembly process through existing tasks. 245 | 246 | ```ruby 247 | module Quarto 248 | class TxtOutput < Plugin 249 | 250 | def enhance_build(build) 251 | # alter the build object 252 | end 253 | 254 | def finalize_build(build) 255 | # alter the build object 256 | end 257 | 258 | def define_tasks 259 | # add rake tasks 260 | desc "Generate TXT files from the source" 261 | task :generate_txt => generate_text 262 | end 263 | 264 | # Add support methods as you need 265 | def generate_text 266 | #... your implementation 267 | end 268 | 269 | end 270 | end 271 | ``` 272 | 273 | You can access your plugin through the Quarto config object: 274 | 275 | ```ruby 276 | Quarto.configure do |config| 277 | config.use :text_output 278 | end 279 | ``` 280 | 281 | Quarto will take the argument from the `config.use` call to determine the name of your plugin class. The argument `:text_output` will be translated to `"TextOutput"` 282 | 283 | When hooking into the config object, Quarto expects your plugin to be stored (using this example) in "quarto/text_output". Your plugin should be created under the Quarto namespace. Quarto will attempt to find it using `Quarto.const_get` meaning it will look inside its own namespace. 284 | 285 | ## Contributing 286 | 287 | 1. Fork it 288 | 2. Create your feature branch (`git checkout -b my-new-feature`) 289 | 3. Commit your changes (`git commit -am 'Add some feature'`) 290 | 4. Push to the branch (`git push origin my-new-feature`) 291 | 5. Create new Pull Request 292 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/clean" 2 | require "bundler/gem_tasks" 3 | require "rspec/core/rake_task" 4 | require "yard" 5 | require_relative "spec/env" 6 | 7 | RSpec::Core::RakeTask.new(:spec) do |t| 8 | t.rspec_opts = "-t ~org" 9 | end 10 | 11 | YARD::Rake::YardocTask.new do |t| 12 | t.options = [] 13 | end 14 | 15 | CLEAN << VENDOR_ORG_MODE_DIR << 16 | "vendor/org-#{ORG_VERSION}" 17 | CLOBBER << "vendor/org-#{ORG_VERSION}.tar.gz" 18 | 19 | task :default => :spec 20 | task :spec => :vendor_orgmode 21 | task :vendor_orgmode => VENDOR_ORG_MODE_DIR 22 | 23 | file VENDOR_ORG_MODE_DIR => "vendor/org-#{ORG_VERSION}" do |t| 24 | mkdir_p File.expand_path("..", VENDOR_ORG_MODE_DIR) 25 | ln_sf File.expand_path("vendor/org-#{ORG_VERSION}"), VENDOR_ORG_MODE_DIR 26 | end 27 | 28 | directory "vendor/org-#{ORG_VERSION}" => "vendor/org-#{ORG_VERSION}.tar.gz" do |t| 29 | cd "vendor" do 30 | sh "tar -xzf org-#{ORG_VERSION}.tar.gz" 31 | end 32 | cd "vendor/org-#{ORG_VERSION}" do 33 | sh "make" 34 | end 35 | end 36 | 37 | file "vendor/org-#{ORG_VERSION}.tar.gz" => "vendor" do |t| 38 | cd "vendor" do 39 | "http://orgmode.org/org-#{ORG_VERSION}.tar.gz".tap do |url| 40 | sh "which wget && wget #{url} || curl -O #{url}" 41 | end 42 | end 43 | end 44 | 45 | directory "vendor" 46 | -------------------------------------------------------------------------------- /bin/golden: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "optparse" 4 | $:.unshift(File.expand_path("../../lib", __FILE__)) 5 | require "golden_child" 6 | 7 | command = ARGV.shift 8 | 9 | options = OptionParser.new do |o| 10 | o.banner = < error 45 | abort error.message 46 | end 47 | -------------------------------------------------------------------------------- /examples/images/Rakefile: -------------------------------------------------------------------------------- 1 | require 'quarto' 2 | Quarto.configure do |config| 3 | config.clear_stylesheets 4 | config.use :markdown 5 | config.metadata = false 6 | end 7 | -------------------------------------------------------------------------------- /examples/images/ch1.md: -------------------------------------------------------------------------------- 1 |

Before listing 0

2 | ```ruby 3 | puts "hello, world" 4 | ``` 5 |

After listing 0

6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/images/images/image1.png: -------------------------------------------------------------------------------- 1 | PRETEND I'M AN IMAGE 2 | -------------------------------------------------------------------------------- /examples/markdown-basic/Rakefile: -------------------------------------------------------------------------------- 1 | require "quarto/tasks" 2 | -------------------------------------------------------------------------------- /examples/markdown-basic/empty.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avdi/quarto/5ebc3a064c565ea3b129ae3617b9484529d632cd/examples/markdown-basic/empty.md -------------------------------------------------------------------------------- /examples/markdown-basic/intro.md: -------------------------------------------------------------------------------- 1 | # Hello, world 2 | 3 | This is the adequate intro 4 | -------------------------------------------------------------------------------- /examples/markdown-basic/part1/ch1.md: -------------------------------------------------------------------------------- 1 | # Hello again 2 | 3 | This is chapter 1 4 | -------------------------------------------------------------------------------- /examples/metadata/README: -------------------------------------------------------------------------------- 1 | This project contains a Rakefile that disables stylesheets and metadata 2 | insertion. 3 | -------------------------------------------------------------------------------- /examples/metadata/Rakefile: -------------------------------------------------------------------------------- 1 | require 'quarto/tasks' 2 | Quarto.configure do |config| 3 | config.stylesheets.clear 4 | config.metadata = true 5 | 6 | config.author = "Avdi Grimm" 7 | config.title = "Hello World, The Book" 8 | config.description = "The greatest book ever written" 9 | config.language = "en-US" 10 | config.date = "2013-08-01" 11 | end 12 | -------------------------------------------------------------------------------- /examples/metadata/intro.md: -------------------------------------------------------------------------------- 1 | # Hello, world 2 | 3 | This is the intro 4 | -------------------------------------------------------------------------------- /examples/metadata/part1/ch1.md: -------------------------------------------------------------------------------- 1 | # Hello again 2 | 3 | This is chapter 1 4 | -------------------------------------------------------------------------------- /examples/minimal/README: -------------------------------------------------------------------------------- 1 | This project contains a Rakefile that disables stylesheets and metadata 2 | insertion. 3 | -------------------------------------------------------------------------------- /examples/minimal/Rakefile: -------------------------------------------------------------------------------- 1 | require 'quarto/tasks' 2 | Quarto.configure do |config| 3 | config.stylesheets.clear 4 | config.metadata = false 5 | end 6 | -------------------------------------------------------------------------------- /examples/minimal/intro.md: -------------------------------------------------------------------------------- 1 | # Hello, world 2 | 3 | This is the intro 4 | -------------------------------------------------------------------------------- /examples/minimal/part1/ch1.md: -------------------------------------------------------------------------------- 1 | # Hello again 2 | 3 | This is chapter 1 4 | -------------------------------------------------------------------------------- /examples/orgmode-basic/Rakefile: -------------------------------------------------------------------------------- 1 | require 'quarto' 2 | 3 | Quarto.configure do |config| 4 | config.use :orgmode 5 | 6 | # Note: this line is included for testing purposes. It shouldn't be needed in 7 | # your Quarto projects. 8 | config.orgmode.emacs_load_path << ENV.fetch("VENDOR_ORG_MODE_DIR") 9 | end 10 | -------------------------------------------------------------------------------- /examples/orgmode-basic/book.org: -------------------------------------------------------------------------------- 1 | * Chapter 1 2 | 3 | Hello from Org-Mode! 4 | 5 | #+BEGIN_SRC ruby 6 | puts 1 + 1 7 | #+END_SRC 8 | 9 | -------------------------------------------------------------------------------- /examples/pandoc_epub/Rakefile: -------------------------------------------------------------------------------- 1 | require "quarto" 2 | 3 | Quarto.configure do |c| 4 | c.use :markdown 5 | c.use :pandoc_epub 6 | end 7 | -------------------------------------------------------------------------------- /examples/pandoc_epub/ch1.md: -------------------------------------------------------------------------------- 1 | # The Great American Novel 2 | 3 | It was a dark and stormy night. 4 | 5 | -------------------------------------------------------------------------------- /examples/source-listings/Rakefile: -------------------------------------------------------------------------------- 1 | require 'quarto/tasks' 2 | -------------------------------------------------------------------------------- /examples/source-listings/ch1.md: -------------------------------------------------------------------------------- 1 | ```ruby 2 | puts "hello, world" 3 | ``` 4 | -------------------------------------------------------------------------------- /examples/source-listings/ch2.md: -------------------------------------------------------------------------------- 1 | ```c 2 | int main(int argc, char** argv) { 3 | printf("Hello, world\n") 4 | } 5 | ``` 6 | -------------------------------------------------------------------------------- /examples/structure/README: -------------------------------------------------------------------------------- 1 | This example project is intended to demonstrate how Quarto builds a book 2 | structure from source files. 3 | -------------------------------------------------------------------------------- /examples/structure/Rakefile: -------------------------------------------------------------------------------- 1 | require 'quarto' 2 | 3 | Quarto.configure do |config| 4 | config.use :orgmode 5 | config.use :markdown 6 | 7 | # Note: this line is included for testing purposes. It shouldn't be needed in 8 | # your Quarto projects. 9 | config.orgmode.emacs_load_path << ENV.fetch("VENDOR_ORG_MODE_DIR") 10 | 11 | config.source_files = [ 12 | "foreward.markdown", 13 | "preface.markdown", 14 | "ch1.markdown", 15 | "ch2.org", 16 | "ch3-implicit.markdown", 17 | "ch4-implicit.org", 18 | "subdir/ch5.markdown", 19 | ] 20 | end 21 | -------------------------------------------------------------------------------- /examples/structure/ch1.markdown: -------------------------------------------------------------------------------- 1 | This text precedes any section tag and should be thrown out. 2 | 3 |
4 | 5 | # Chapter 1 6 | 7 | This is chapter 1, written in Markdown. 8 | 9 | ## Section 1.1 10 | 11 | ### Subsection 1.1.1 12 | 13 | #### Subsubsection 1.1.1.1 14 | 15 | ##### Subsubsubsection 1.1.1.1.1 16 | 17 | ## Section 1.2 18 | 19 | ### Subsection 1.2.1 20 | 21 | #### Subsubsection 1.2.1.1 22 | 23 | ##### Subsubsubsection 1.2.1.1.1 24 | 25 |
26 | 27 | This text is after any section tag, and should be thrown out. 28 | -------------------------------------------------------------------------------- /examples/structure/ch2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This the title of ch2.org 6 | 7 | 8 | 9 | 10 | 88 | 134 | 135 | 136 |
137 |

This the title of ch2.org

138 |
139 |

Table of Contents

140 |
141 | 157 |
158 |
159 |

160 | This text precedes the chapter, and should be discarded. 161 |

162 | 163 |
164 |

1 Chapter 2

165 |
166 | 167 |

168 | This is chapter 2, written in Org-Mode. 169 |

170 | 171 |

172 | This is a link to the chapter head. 173 |

174 |
175 | 176 |
177 |

1.1 Section 2.1

178 |
179 |
180 |

1.1.1 Subsection 2.1.1

181 |
182 |
  1. Subsubsection 2.1.1.1
    1. Subsubsubscetion 2.1.1.1.1
    183 |
184 |
185 |
186 |
187 |

1.2 Section 2.2

188 |
189 |
190 |

1.2.1 Subsection 2.2.1

191 |
192 |
  1. Subsubsection 2.2.1.1
    1. Subsubsubscetion 2.2.1.1.1
    193 |
194 |
195 |
196 |
197 |
198 |
199 |

Author: Avdi Grimm

200 |

Created: 2014-06-29 Sun 21:44

201 |

Emacs 24.3.1 (Org mode 8.0.3)

202 |

Validate XHTML 1.0

203 |
204 | 205 | 206 | -------------------------------------------------------------------------------- /examples/structure/ch2.org: -------------------------------------------------------------------------------- 1 | #+TITLE: This the title of ch2.org 2 | 3 | This text precedes the chapter, and should be discarded. 4 | 5 | * Chapter 2 6 | :PROPERTIES: 7 | :HTML_CONTAINER: section 8 | :HTML_CONTAINER_CLASS: chapter 9 | :ID: 4f9b09d9-6733-4273-b271-e735e41c764d 10 | :END: 11 | 12 | This is chapter 2, written in Org-Mode. 13 | 14 | [[id:4f9b09d9-6733-4273-b271-e735e41c764d][This is a link to the chapter head.]] 15 | 16 | ** Section 2.1 17 | 18 | *** Subsection 2.1.1 19 | 20 | **** Subsubsection 2.1.1.1 21 | 22 | ***** Subsubsubscetion 2.1.1.1.1 23 | 24 | ** Section 2.2 25 | 26 | *** Subsection 2.2.1 27 | 28 | **** Subsubsection 2.2.1.1 29 | 30 | ***** Subsubsubscetion 2.2.1.1.1 31 | -------------------------------------------------------------------------------- /examples/structure/ch3-implicit.markdown: -------------------------------------------------------------------------------- 1 | % Title of Chapter 3 2 | % Tom Servo 3 | % June 29, 2014 4 | 5 | # First heading in chapter 3 6 | 7 | This chapter is not contained in an explicit SECTION tag. -------------------------------------------------------------------------------- /examples/structure/ch4-implicit.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Title of Chapter 4 2 | #+AUTHOR: Crow T. Robot 3 | #+DATE: July 5, 2014 4 | #+DESCRIPTION: Chapter 4 is the best chapter since chapter 3. 5 | 6 | * First heading of chapter 4 7 | 8 | This chapter is not explicitly marked as a chapter with an org-mode property. 9 | -------------------------------------------------------------------------------- /examples/structure/foreward.markdown: -------------------------------------------------------------------------------- 1 |
2 | 3 | # The Foreword 4 | 5 | This is the foreword. 6 | 7 |
8 | -------------------------------------------------------------------------------- /examples/structure/preface.markdown: -------------------------------------------------------------------------------- 1 |
2 | 3 | # The Preface 4 | 5 | This is the preface. 6 | 7 |
8 | -------------------------------------------------------------------------------- /examples/structure/subdir/ch5.markdown: -------------------------------------------------------------------------------- 1 | # Chapter 5 2 | 3 | This chapter is in a subdirectory. -------------------------------------------------------------------------------- /examples/website/Rakefile: -------------------------------------------------------------------------------- 1 | require "quarto" 2 | 3 | Quarto.configure do |config| 4 | config.use :markdown 5 | config.use :site 6 | end 7 | -------------------------------------------------------------------------------- /examples/website/ch1.markdown: -------------------------------------------------------------------------------- 1 | # Chapter 1: An unexpected pancake 2 | 3 | In which our hero receives a surprise at breakfast time. 4 | -------------------------------------------------------------------------------- /examples/website/ch2.markdown: -------------------------------------------------------------------------------- 1 | # Chapter 2: Socks and Violets 2 | 3 | In which our hero goes unshod through the flower bed. 4 | -------------------------------------------------------------------------------- /fontforge/convert.pe: -------------------------------------------------------------------------------- 1 | Open($1) 2 | Generate($2) 3 | -------------------------------------------------------------------------------- /lib/quarto.rb: -------------------------------------------------------------------------------- 1 | require "quarto/version" 2 | require "quarto/build" 3 | require "quarto/plugin" 4 | require "dotenv" 5 | 6 | module Quarto 7 | def self.build 8 | verbosity = if Object.const_defined?(:Rake) 9 | Rake::FileUtilsExt.verbose 10 | else 11 | true 12 | end 13 | @build ||= new_build(verbose: verbosity) 14 | end 15 | 16 | def self.new_build(options={}) 17 | Build.new do |b| 18 | b.verbose = options[:verbose] 19 | end 20 | end 21 | 22 | def self.method_missing(method_name, *args, &block) 23 | build.public_send(method_name, *args, &block) 24 | end 25 | 26 | def build 27 | ::Quarto.build 28 | end 29 | 30 | def self.configure 31 | load_environment 32 | yield build 33 | build.finalize 34 | build.define_tasks 35 | end 36 | 37 | def self.reconfigure(&block) 38 | Rake.application.clear 39 | reset 40 | configure(&block) 41 | end 42 | 43 | def self.reset 44 | @build = nil 45 | end 46 | 47 | def self.load_environment 48 | Dotenv.load 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/quarto/bower.rb: -------------------------------------------------------------------------------- 1 | require "quarto/plugin" 2 | 3 | module Quarto 4 | class Bower < Plugin 5 | module BuildExt 6 | attr_accessor :bower 7 | end 8 | 9 | attr_reader :deps 10 | 11 | def initialize(*) 12 | super 13 | @deps = [] 14 | end 15 | 16 | def enhance_build(build) 17 | build.require_plugin(:template_set) 18 | build.extend(BuildExt) 19 | build.bower = self 20 | end 21 | 22 | def define_tasks 23 | namespace :bower do 24 | desc "Install Bower dependencies" 25 | task :install => [config_file, package_file] do 26 | deps.each do |dep| 27 | cd main.build_dir do 28 | sh "bower install -S #{dep}" 29 | end 30 | end 31 | end 32 | end 33 | end 34 | 35 | def add_dep(package) 36 | deps << package 37 | end 38 | 39 | def config_file 40 | "#{main.build_dir}/.bowerrc" 41 | end 42 | 43 | def package_file 44 | "#{main.build_dir}/bower.json" 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/quarto/build.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require "rake" 3 | require "nokogiri" 4 | require "open3" 5 | require "digest/sha1" 6 | require "etc" 7 | require "fattr" 8 | require "time" 9 | require "erb" 10 | require "quarto/font" 11 | require "quarto/stylesheet_set" 12 | require "pathname" 13 | require "ostruct" 14 | require "yaml" 15 | 16 | module Quarto 17 | 18 | # TODO: For the love of all that is holy, refactor me!!! 19 | class Build 20 | include Rake::DSL 21 | 22 | XINCLUDE_NS = "http://www.w3.org/2001/XInclude" 23 | XHTML_NS = "http://www.w3.org/1999/xhtml" 24 | DC_NS = "http://purl.org/dc/elements/1.1/" 25 | 26 | NAMESPACES = { 27 | "xhtml" => XHTML_NS, 28 | "xi" => XINCLUDE_NS, 29 | } 30 | 31 | SIGNATURE_TEMPLATE = <<-EOF 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | EOF 41 | 42 | SPINE_TEMPLATE = <<-EOF 43 | 44 | 45 | 46 | Untitled Book 47 | 48 | 49 | 50 | 51 | 52 | EOF 53 | 54 | fattr :verbose => true 55 | fattr :metadata => true 56 | fattr(:authors) { 57 | [Etc.getpwnam(Etc.getlogin).gecos.split(',')[0]] 58 | } 59 | fattr :title => "Untitled Book" 60 | fattr(:name) { title.downcase.tr_s("^a-z0-9", "-") } 61 | fattr :description => "" 62 | fattr :language => "en" 63 | fattr(:date) { Time.now.iso8601 } 64 | fattr(:rights) { 65 | "Copyright © #{Time.parse(date).year} #{author}" 66 | } 67 | fattr(:extensions_to_source_formats) { {} } 68 | fattr(:plugins) { {} } 69 | fattr(:deliverable_files) { FileList[] } 70 | fattr(:extra_asset_files) { FileList[] } 71 | fattr(:all_master_files) { 72 | FileList[ 73 | master_file, 74 | assets_file, 75 | ] 76 | } 77 | fattr(:fonts) { [] } 78 | fattr(:bitmap_cover_image) { nil } 79 | fattr(:vector_cover_image) { nil } 80 | fattr(:toplevel_classes) { 81 | mainmatter_classes | nonchapter_classes 82 | } 83 | fattr(:frontmatter_classes) { 84 | %W[frontcover halftitlepage titlepage imprint dedication foreword 85 | toc preface introduction] 86 | } 87 | fattr(:backmatter_classes) { 88 | %W[references appendix bibliography glossary index colophon backcover] 89 | } 90 | fattr(:mainmatter_classes) { 91 | %W[chapter] 92 | } 93 | fattr(:nonchapter_classes) { 94 | frontmatter_classes | backmatter_classes 95 | } 96 | fattr(:build_dir) { 97 | "build" 98 | } 99 | fattr(:configured) { false } 100 | 101 | def initialize 102 | use :stylesheet_set 103 | yield self if block_given? 104 | end 105 | 106 | def use(plugin_name, *args, &block) 107 | plugin_class = find_plugin_class(plugin_name) 108 | plugin = plugin_class.new(self, *args, &block) 109 | plugin.enhance_build(self) 110 | plugins[plugin_name.to_sym] = plugin 111 | end 112 | 113 | def find_plugin_class(plugin_name) 114 | require "quarto/#{plugin_name}" 115 | plugin_class_name = 116 | plugin_name.to_s.split("_").map { |w| w.capitalize }.join 117 | Quarto.const_get(plugin_class_name) 118 | end 119 | 120 | def add_font(family, options={}) 121 | fonts << Font.new(family, options) 122 | end 123 | 124 | def define_tasks 125 | define_main_tasks 126 | define_plugin_tasks 127 | end 128 | 129 | def finalize 130 | plugins.values.each do |plugin| 131 | plugin.finalize_build(self) 132 | end 133 | end 134 | 135 | def source_exclusions 136 | @source_exclusions ||= ["#{build_dir}/**/*"] 137 | end 138 | 139 | def exclude_sources(*exclusion_patterns) 140 | source_files.exclude(*exclusion_patterns) 141 | end 142 | 143 | def source_files=(new_source_files) 144 | @source_files = Rake::FileList[*new_source_files] 145 | end 146 | 147 | def author=(sole_author) 148 | self.authors = [sole_author] 149 | end 150 | 151 | def author 152 | authors.join(", ") 153 | end 154 | 155 | def source_exts 156 | extensions_to_source_formats.keys 157 | end 158 | 159 | def format_of_source_file(source_file) 160 | ext = source_file.pathmap("%x")[1..-1] 161 | extensions_to_source_formats.fetch(ext) 162 | end 163 | 164 | def source_files 165 | @source_files ||= FileList.new do |files| 166 | files.exclude(*source_exclusions) 167 | end 168 | end 169 | 170 | def export_dir 171 | "#{build_dir}/exports" 172 | end 173 | 174 | def export_files 175 | source_files.pathmap("#{export_dir}/%p").ext('.html') 176 | end 177 | 178 | def source_for_export_file(export_file) 179 | base = export_file.sub(/^#{export_dir}\//, '').ext('') 180 | pattern = "#{base}.{#{source_exts.join(',')}}" 181 | FileList[pattern].first 182 | end 183 | 184 | def export(export_file, source_file) 185 | format = format_of_source_file(source_file) 186 | send("export_from_#{format}", export_file, source_file) 187 | end 188 | 189 | def signature_dir 190 | "#{build_dir}/signatures" 191 | end 192 | 193 | def signature_files 194 | export_files.pathmap("%{^#{export_dir},#{signature_dir}}X%{html,xhtml}x") 195 | end 196 | 197 | def export_for_signature_file(signature_file) 198 | signature_file.pathmap("%{^#{signature_dir},#{export_dir}}X%{xhtml,html}x") 199 | end 200 | 201 | def normalize_export(export_file, section_file, format) 202 | format ||= "NO_FORMAT_GIVEN" 203 | send("normalize_#{format}_export", export_file, section_file) 204 | end 205 | 206 | def normalize_generic_export(export_file, signature_file, before: nil) 207 | say("normalize #{export_file} to #{signature_file}") 208 | doc = open(export_file) do |f| 209 | Nokogiri::HTML(f) 210 | end 211 | 212 | before.call(doc) if before 213 | 214 | name = export_file.pathmap("%n") 215 | title = title_from_doc(doc) 216 | normal_doc = Nokogiri::XML.parse(SIGNATURE_TEMPLATE) 217 | body_elt = normal_doc.at_css("body") 218 | export_body_elt = doc.at_css("body") 219 | export_content = export_body_elt && export_body_elt.xpath("section") 220 | if export_content.empty? 221 | chapter_contents = export_body_elt.children.dup 222 | export_content = manufacture_chapter(chapter_contents, normal_doc, 223 | title) 224 | end 225 | source_file = 226 | FileList[export_file.pathmap("%{^#{export_dir}/,}X.*")].first 227 | signature_elt = body_elt.add_child( 228 | normal_doc.create_element("div") do |elt| 229 | elt["class"] = "signature" 230 | elt["data-signature-export"] = export_file 231 | elt["data-signature-source"] = source_file 232 | elt["data-signature-file"] = signature_file 233 | elt["data-signature-name"] = name 234 | elt["data-signature-title"] = title 235 | elt["data-name"] = name 236 | elt["data-title"] = title 237 | end) 238 | 239 | signature_elt.add_child(export_content) 240 | normal_doc.at_css("title").content = title 241 | yield(normal_doc) if block_given? 242 | mark_toplevel_sections(normal_doc) 243 | extract_titles(normal_doc) 244 | open(signature_file, "w") do |f| 245 | format_xml(f) do |pipe_input| 246 | normal_doc.write_xml_to(pipe_input) 247 | end 248 | end 249 | end 250 | 251 | def manufacture_chapter(chapter_contents, doc, title) 252 | title = title == "Untitled Signature" ? "Untitled Chapter" : title 253 | doc.create_element("section", 254 | "class" => "chapter", 255 | "data-title" => title, 256 | "data-name" => name_from_title(title)) do |elt| 257 | elt.children = chapter_contents 258 | end 259 | end 260 | 261 | def title_from_doc(doc) 262 | title_from_head(doc) || title_from_content(doc) || "Untitled Signature" 263 | end 264 | 265 | def title_from_head(doc) 266 | title_elt = doc.at_css("title") 267 | title = title_elt && title_elt.text.strip 268 | unless title.nil? || title.empty? 269 | title 270 | end 271 | end 272 | 273 | def title_from_content(doc) 274 | headers = doc.css("h1, h2, h3, h4, h5, h6") 275 | first_header = headers.first 276 | header_title = first_header && first_header.text.strip 277 | unless header_title.nil? || header_title.empty? 278 | header_title 279 | end 280 | end 281 | 282 | # At some point I stopped getting missing body elements for blank 283 | # markdown documents, and started getting a body element with a 284 | # single empty P tag instead. I'm not sure if this was a change in 285 | # Pandoc, a change in libxml2, or something else. I don't really 286 | # care either; this helper handles both the missing element and 287 | # the boilerplate cases. 288 | def body_has_meaningful_content?(body_elt) 289 | body_elt && body_elt.to_s != "

" 290 | end 291 | 292 | def mark_toplevel_sections(doc) 293 | selector = toplevel_classes.map { |c| "section[class~=#{c}]" }.join(",") 294 | doc.css(selector).each do |elt| 295 | elt["class"] = add_css_classes(elt, "toplevel") 296 | end 297 | end 298 | 299 | def add_css_classes(elt, *new_classes) 300 | (elt["class"].to_s.split + new_classes).join(" ") 301 | end 302 | 303 | def extract_titles(doc) 304 | doc.css("section.toplevel").each do |section_elt| 305 | type = toplevel_type_from_element(section_elt) 306 | first_heading = section_elt.at_css("h1") 307 | title = title_from_element(first_heading, type) 308 | section_elt["data-title"] ||= title 309 | section_elt["data-name"] ||= name_from_title(title) 310 | end 311 | end 312 | 313 | def title_from_element(element, type="item") 314 | title = element && element.text.strip 315 | if title.nil? || title.empty? 316 | title = "Untitled #{type.capitalize}" 317 | end 318 | title 319 | end 320 | 321 | def toplevel_type_from_element(element) 322 | (element["class"].split & toplevel_classes).first || "item" 323 | end 324 | 325 | def region_for_toplevel_type(toplevel_type) 326 | case toplevel_type 327 | when *frontmatter_classes then 328 | "frontmatter" 329 | when *backmatter_classes then 330 | "backmatter" 331 | when *mainmatter_classes then 332 | "mainmatter" 333 | else 334 | fail ArgumentError, "Unknown type #{toplevel_type}" 335 | end 336 | end 337 | 338 | def name_from_title(title) 339 | title.downcase.tr_s("^a-z0-9", " ").strip.tr(" ", "-") 340 | end 341 | 342 | def source_list_file 343 | "#{build_dir}/sources" 344 | end 345 | 346 | def spine_file 347 | "#{build_dir}/spine.xhtml" 348 | end 349 | 350 | def create_spine_file(spine_file, signature_files, options={}) 351 | options = { 352 | stylesheets: stylesheets, 353 | metadata: metadata 354 | }.merge(options) 355 | say("create #{spine_file} from sections: #{signature_files}") 356 | doc = Nokogiri::XML.parse(SPINE_TEMPLATE) 357 | doc.root.at_css("title").content = title 358 | add_metadata_to_doc(doc) if options[:metadata] 359 | doc.root.add_namespace("xi", "http://www.w3.org/2001/XInclude") 360 | head_elt = doc.root.at_css("head") 361 | stylesheets = options[:stylesheets] 362 | stylesheets.each do |stylesheet| 363 | head_elt.add_child(stylesheet.link_tag) 364 | end 365 | signature_files.each do |section_file| 366 | doc.root["xml:base"] = ".." 367 | body = doc.root.at_css("body") 368 | body.add_child(doc.create_element("xi:include") do |inc_elt| 369 | inc_elt["href"] = section_file 370 | inc_elt["xpointer"] = "xmlns(ns=http://www.w3.org/1999/xhtml)xpointer(//ns:body/*)" 371 | inc_elt.add_child(doc.create_element("xi:fallback") do |fallback_elt| 372 | fallback_elt.add_child(doc.create_element("p", 373 | "[Missing section: #{section_file}]")) 374 | end) 375 | end) 376 | end 377 | open(spine_file, 'w') do |f| 378 | format_xml(f) do |format_input| 379 | doc.write_to(format_input) 380 | end 381 | end 382 | end 383 | 384 | def add_metadata_to_doc(doc) 385 | head_elt = doc.at_css("head") 386 | add_metadata_element(doc, head_elt, "author", authors.join(", ")) 387 | add_metadata_element(doc, head_elt, "date", date) 388 | add_metadata_element(doc, head_elt, "subject", description) 389 | add_metadata_element(doc, head_elt, "generator", "Quarto #{Quarto::VERSION}") 390 | add_metadata_element(doc, head_elt, "DC.title", title) 391 | add_metadata_element(doc, head_elt, "DC.creator", authors) 392 | add_metadata_element( 393 | doc, head_elt, "DC.description", description) 394 | add_metadata_element(doc, head_elt, "DC.date", date) 395 | add_metadata_element(doc, head_elt, "DC.language", language) 396 | add_metadata_element(doc, head_elt, "DC.rights", rights) 397 | end 398 | 399 | def add_metadata_element(doc, parent, name, value) 400 | Array(value).each do |value| 401 | parent.add_child(doc.create_element("meta") do |meta| 402 | meta["name"] = name 403 | meta["content"] = value 404 | end) 405 | end 406 | end 407 | 408 | def codex_file 409 | "#{build_dir}/codex.xhtml" 410 | end 411 | 412 | def create_codex_file(codex_file, spine_file) 413 | mkdir_p(codex_file.pathmap("%d")) 414 | proto_codex_file = codex_file.pathmap("%d/proto-%f") 415 | expand_xinclude(proto_codex_file, spine_file, format: false) 416 | proto_doc = Nokogiri::XML(File.read(proto_codex_file)) 417 | update_signature_elements(proto_doc) 418 | update_toplevel_elements(proto_doc) 419 | update_heading_elements(proto_doc) 420 | number_chapters(proto_doc) 421 | open(codex_file, "w") do |f| 422 | proto_doc.write_xml_to(f) 423 | end 424 | end 425 | 426 | def number_chapters(doc) 427 | doc.css("section.chapter").each_with_index do |chap_elt, index| 428 | chap_elt["data-chapter-number"] = index + 1 429 | end 430 | end 431 | 432 | def skeleton_file 433 | "#{build_dir}/skeleton.xhtml" 434 | end 435 | 436 | def listings_dir 437 | "#{build_dir}/listings" 438 | end 439 | 440 | def create_skeleton_file(skeleton_file, codex_file) 441 | say("scan #{codex_file} for source code listings") 442 | skel_doc = open(codex_file) do |f| 443 | Nokogiri::XML(f) 444 | end 445 | skel_doc.css("pre.sourceCode").each_with_index do |pre_elt, i| 446 | classes = pre_elt["class"].split 447 | classes.delete("sourceCode") 448 | unless classes.size == 1 449 | raise "Ambiguous source code language in classes: #{classes}" 450 | end 451 | lang = classes.first 452 | ext = {"ruby" => "rb"}.fetch(lang) { lang.downcase } 453 | code = strip_listing(pre_elt.at_css("code").text) 454 | digest = Digest::SHA1.hexdigest(code) 455 | listing_path = "#{listings_dir}/#{digest}.#{ext}" 456 | if File.exist?(listing_path) 457 | say "skip extant listing #{listing_path}" 458 | else 459 | say("extract listing #{i} to #{listing_path}") 460 | open(listing_path, 'w') do |f| 461 | f.write(code) 462 | end 463 | end 464 | highlight_path = "#{highlights_dir}/#{digest}.html" 465 | inc_elt = skel_doc.create_element("xi:include") do |elt| 466 | elt["href"] = highlight_path 467 | elt.add_child( 468 | ""\ 469 | "

[Missing code listing: #{highlight_path}]

"\ 470 | "
") 471 | end 472 | pre_elt.replace(inc_elt) 473 | end 474 | say("create #{skeleton_file}") 475 | open(skeleton_file, "w") do |f| 476 | format_xml(f) do |format_input| 477 | skel_doc.write_xml_to(format_input) 478 | end 479 | end 480 | end 481 | 482 | def highlights_file 483 | "#{build_dir}/highlights.timestamp" 484 | end 485 | 486 | def highlights_dir 487 | "#{build_dir}/highlights" 488 | end 489 | 490 | def highlights_needed_by(skeleton_file) 491 | doc = open(skeleton_file) do |f| 492 | Nokogiri::XML(f) 493 | end 494 | doc.xpath("//xi:include", NAMESPACES).map { |e| e["href"] } 495 | end 496 | 497 | def listing_for_highlight_file(highlight_file) 498 | base = highlight_file.pathmap("%n") 499 | FileList["#{listings_dir}/#{base}.*"].first 500 | end 501 | 502 | # Strip extraneous whitespace from around a code listing 503 | def strip_listing(code) 504 | code = code.dup 505 | code.gsub!(/\t/, " ") 506 | lines = code.split("\n") 507 | first_code_line = lines.index { |l| l =~ /\S/ } 508 | last_code_line = lines.rindex { |l| l =~ /\S/ } 509 | code_lines = lines[first_code_line..last_code_line] 510 | line_indents = code_lines.map { |l| l.index(/\S/) || 0 } 511 | min_indent = line_indents.min 512 | unindented_code = code_lines.map { |l| l[min_indent..-1] }.join("\n") 513 | unindented_code.strip 514 | end 515 | 516 | def master_file 517 | "#{master_dir}/master.xhtml" 518 | end 519 | 520 | def master_dir 521 | "#{build_dir}/master" 522 | end 523 | 524 | def create_master_file(master_file, skeleton_file) 525 | mkdir_p(master_file.pathmap("%d")) 526 | expand_xinclude(master_file, skeleton_file, format: false) 527 | end 528 | 529 | def update_signature_elements(doc) 530 | doc.css(".signature").each_with_index do |signature_elt, index| 531 | name = signature_elt["data-signature-name"] or 532 | fail "Missing signature name" 533 | number = index + 1 534 | 535 | signature_elt["data-signature-number"] = number 536 | signature_elt["data-number"] = number 537 | signature_elt["id"] = "signature-#{number}" 538 | signature_elt["data-fascicle"] = fascicle_file(name, number) 539 | signature_elt["data-numbered-name"] = numbered_name(name, number) 540 | end 541 | end 542 | 543 | def update_toplevel_elements(doc) 544 | toplevel_number = 1 545 | doc.css(".signature").each do |sig_elt| 546 | fasc_file = sig_elt["data-fascicle"] 547 | sig_num = sig_elt["data-number"] or fail "Signature is not numbered" 548 | sig_elt.css("section.toplevel").each_with_index do |top_elt, index| 549 | number = index + 1 550 | type = toplevel_type_from_element(top_elt) 551 | top_elt["id"] = "toplevel-#{toplevel_number}" 552 | top_elt["data-number"] = number 553 | top_elt["data-fascicle"] = fasc_file 554 | top_elt["data-toplevel-number"] = toplevel_number 555 | toplevel_number += 1 556 | end 557 | end 558 | end 559 | 560 | def update_heading_elements(doc) 561 | replacements = {} 562 | doc.css("section.toplevel").each do |top_elt| 563 | top_num = top_elt["data-number"] or fail "Element is not numbered" 564 | top_id = top_elt["id"] or fail "Element is missing ID" 565 | top_elt.css("h1, h2, h3, h4, h5, h6").each_with_index do 566 | |heading_elt, index| 567 | old_id = heading_elt["id"] 568 | new_id = top_id + "-heading-#{index + 1}" 569 | heading_elt["id"] = new_id 570 | replacements[old_id] = new_id 571 | end 572 | end 573 | doc.css("a[href^='#']").each do |link_elt| 574 | target_id = link_elt["href"][1..-1] 575 | if (new_id = replacements[target_id]) 576 | link_elt["href"] = "##{new_id}" 577 | end 578 | end 579 | end 580 | 581 | def fascicle_manifest 582 | "#{build_dir}/fascicle-manifest.txt" 583 | end 584 | 585 | def fascicle_dir 586 | "#{build_dir}/fascicles" 587 | end 588 | 589 | def extract_fascicles(master_file, fascicle_manifest) 590 | master_doc = open(master_file) do |f| 591 | Nokogiri::XML(f) 592 | end 593 | fasc_elts = master_doc.css(".signature") 594 | paths = [] 595 | fasc_elts.each_with_index { |elt, index| 596 | name = elt["data-signature-name"] 597 | number = index + 1 598 | path = fascicle_file(name, number) 599 | title = elt["data-signature-title"] 600 | fasc_doc = master_doc.dup 601 | 602 | fasc_doc.at_css("body").children = elt.dup 603 | fasc_doc.at_css("title").content = title 604 | mkpath path.pathmap("%d") 605 | open(path, "w") do |f| 606 | say "write #{path}" 607 | fasc_doc.write_xml_to(f) 608 | end 609 | paths << path 610 | } 611 | say "write #{fascicle_manifest}" 612 | open(fascicle_manifest, "w") do |f| 613 | paths.each do |path| 614 | f.puts(path) 615 | end 616 | end 617 | end 618 | 619 | def fascicle_file(name, number) 620 | filename = numbered_name(name, number) + ".xhtml" 621 | "#{fascicle_dir}/#{filename}" 622 | end 623 | 624 | def numbered_name(name, number) 625 | "%03d-%s" % [number, name] 626 | end 627 | 628 | def fascicles 629 | File.read(fascicle_manifest).split.map.with_index { |path, index| 630 | doc = open(path) { |f| Nokogiri::XML(f) } 631 | name = doc.at_css(".signature")["data-signature-name"] 632 | number = index + 1 633 | OpenStruct.new( 634 | path: path, 635 | title: doc.at_css("title").text.strip, 636 | name: name, 637 | number: number, 638 | numbered_name: "%03d-%s" % [number, name]) 639 | } 640 | end 641 | 642 | def assets_file 643 | "#{build_dir}/assets.timestamp" 644 | end 645 | 646 | def copy_assets(master_file, assets_dir) 647 | asset_files = [] 648 | if bitmap_cover_image 649 | asset_files << bitmap_cover_image 650 | end 651 | if vector_cover_image 652 | asset_files << vector_cover_image 653 | end 654 | asset_files.concat(extra_asset_files) 655 | doc = open(master_file) do |f| 656 | Nokogiri::XML(f) 657 | end 658 | asset_elts = doc.css("*[src]") 659 | asset_elts.each do |elt| 660 | asset_path = Pathname(elt["src"]).cleanpath 661 | asset_files << asset_path 662 | end 663 | asset_files.each do |asset_path| 664 | rel_path = Pathname(asset_path).relative_path_from(Pathname(".")) 665 | dest = Pathname(assets_dir) + rel_path 666 | mkdir_p dest.dirname unless dest.dirname.exist? 667 | ln_sf Pathname(asset_path).relative_path_from(dest.dirname), dest 668 | end 669 | end 670 | 671 | def deliverable_dir 672 | "#{build_dir}/deliverables" 673 | end 674 | 675 | def latex_file 676 | "#{deliverable_dir}/book.latex" 677 | end 678 | 679 | def pandoc 680 | "pandoc" 681 | end 682 | 683 | def pandoc_vars 684 | [ 685 | "-Vtitle=#{title}", 686 | "-Vauthor=#{authors.join(', ')}", 687 | "-Vdate=#{date}", 688 | "-Vlang=#{language}" 689 | ] 690 | end 691 | 692 | def vendor_dir 693 | "#{quarto_dir}/vendor" 694 | end 695 | 696 | def quarto_dir 697 | ".quarto" 698 | end 699 | 700 | def expand_template(template_file, output_file) 701 | say "expand #{template_file} to #{output_file}" 702 | File.write(output_file, ERB.new(File.read(template_file)).result(binding)) 703 | end 704 | 705 | def say(*messages) 706 | $stderr.puts(*messages) if verbose 707 | end 708 | 709 | # Require a plugin to be loaded and added. This is mainly for the use of 710 | # other plugins. If you want to add a plugin to your project, 711 | # invoke{#use} directly. 712 | def require_plugin(plugin_name) 713 | return if plugins.key?(plugin_name.to_sym) 714 | plugin_class = find_plugin_class(plugin_name) 715 | use(plugin_name) 716 | end 717 | 718 | def structure_file 719 | "#{build_dir}/structure.yaml" 720 | end 721 | 722 | private 723 | 724 | def format_xml(output_io) 725 | Open3.popen2(*xmllint_command(*%W[--format --xmlout -])) do 726 | |stdin, stdout, wait_thr| 727 | yield(stdin) 728 | stdin.close 729 | IO.copy_stream(stdout, output_io) 730 | end 731 | end 732 | 733 | def expand_xinclude(output_file, input_file, options={}) 734 | options = {format: true}.merge(options) 735 | say("expand #{input_file} to #{output_file}") 736 | cleanup_args = %W[--nsclean --xmlout --nofixup-base-uris] 737 | if options[:format] 738 | cleanup_args << "--format" 739 | end 740 | Open3.pipeline_r( 741 | xmllint_command(*%W[--nofixup-base-uris --xinclude --xmlout #{input_file}]), 742 | # In order to clean up extraneous namespace declarations we need a second 743 | # xmllint process 744 | xmllint_command(*cleanup_args, "-")) do |output, wait_thr| 745 | open(output_file, 'w') do |f| 746 | IO.copy_stream(output, f) 747 | end 748 | end 749 | end 750 | 751 | def xmlflags 752 | if verbose 753 | [] 754 | else 755 | ["--nowarning"] 756 | end 757 | end 758 | 759 | def xmllint_command(*args) 760 | ["xmllint", *xmlflags, *args] 761 | end 762 | 763 | def create_structure_file(file) 764 | say "create #{file}" 765 | File.write(file, YAML.dump(book_structure)) 766 | end 767 | 768 | # Memoized form of {#get_book_structure} 769 | def book_structure 770 | @book_structure ||= get_book_structure 771 | end 772 | 773 | # @return [Hash] a comprehensive, semi-denormalized data structure 774 | # representing the logical structure of the book. 775 | def get_book_structure 776 | root = {} 777 | root["title"] = title 778 | root["authors"] = authors 779 | root["author"] = authors.join(", ") 780 | root["description"] = description 781 | root["date"] = date 782 | root["types"] = ["book"] 783 | root["master_file"] = master_file 784 | root["codex_file"] = codex_file 785 | root["spine_file"] = spine_file 786 | root["skeleton_file"] = skeleton_file 787 | root["base_dir"] = Dir.pwd 788 | root["children"] = [] 789 | open(master_file) do |f| 790 | doc = Nokogiri::XML(f) 791 | doc.css(".signature").each do |sig_elt| 792 | root['children'] << signature = {} 793 | signature['types'] = ["signature"] 794 | signature['id'] = sig_elt["id"] 795 | signature.merge!(data_atttributes_to_hash(sig_elt)) 796 | signature["children"] = [] 797 | sig_elt.css("section.toplevel").each do |top_elt| 798 | signature["children"] << toplevel = {} 799 | toplevel_type = toplevel_type_from_element(top_elt) 800 | region = region_for_toplevel_type(toplevel_type) 801 | toplevel["types"] = ["toplevel", region, toplevel_type] 802 | toplevel["id"] = top_elt["id"] 803 | toplevel.merge!(data_atttributes_to_hash(top_elt)) 804 | end 805 | end 806 | end 807 | root 808 | end 809 | 810 | def data_atttributes_to_hash(element) 811 | element.attributes.keys.each_with_object({}) { |key, h| 812 | if (md = /\Adata-(.*)/.match(key)) 813 | attr_name = md[1] 814 | structure_key = attr_name.tr("-", "_") 815 | value = case attr_name 816 | when /\bnumber\z/ # ends with "number" 817 | Integer(element[key]) 818 | else 819 | element[key] 820 | end 821 | h[structure_key] = value 822 | end 823 | } 824 | end 825 | 826 | def define_main_tasks 827 | task :default => :deliverables 828 | 829 | desc "Prepare for battle! I mean bookbinding." 830 | task :prepare 831 | 832 | desc "Export from source formats to HTML" 833 | task :export => [:prepare, *export_files] 834 | 835 | desc "Generate normalized XHTML versions of exports" 836 | task :signatures => [*signature_files] 837 | 838 | desc "Build a single XHTML file codex combining all signatures" 839 | task :codex => codex_file 840 | 841 | desc "Strip out code listings for highlighting" 842 | task :skeleton => skeleton_file 843 | 844 | desc "Create master file suitable for conversion into deliverable formats" 845 | task :master => [master_file, assets_file] 846 | 847 | desc "Create finished documents suitable for end-users" 848 | task :deliverables => deliverable_files 849 | 850 | desc "Perform source-code highlighting" 851 | task :highlight => highlights_file 852 | 853 | desc "Separate master into smaller chunks" 854 | task :fascicles => fascicle_manifest 855 | 856 | desc "Build complete representaiton of the book structure" 857 | task :structure => structure_file 858 | 859 | file structure_file => ["fascicles"] do 860 | create_structure_file(structure_file) 861 | end 862 | 863 | file fascicle_manifest => master_file do 864 | extract_fascicles(master_file, fascicle_manifest) 865 | end 866 | 867 | file highlights_file => [skeleton_file] do |t| 868 | highlights_needed = highlights_needed_by(skeleton_file) 869 | missing_highlights = highlights_needed - FileList["#{highlights_dir}/*.html"] 870 | sub_task = Rake::MultiTask.new("highlight_dynamic", Rake.application) 871 | sub_task.enhance(missing_highlights.compact) 872 | sub_task.invoke 873 | touch highlights_file 874 | end 875 | 876 | directory build_dir 877 | directory export_dir => [build_dir] 878 | directory deliverable_dir => build_dir 879 | 880 | export_files.each do |export_file| 881 | file export_file => 882 | [export_dir, source_for_export_file(export_file)] do |t| 883 | source_file = source_for_export_file(export_file) 884 | mkdir_p export_file.pathmap("%d") 885 | export(export_file, source_file) 886 | end 887 | end 888 | 889 | signature_files.each do |section_file| 890 | file section_file => export_for_signature_file(section_file) do |t| 891 | export_file = export_for_signature_file(section_file) 892 | source_file = source_for_export_file(export_file) 893 | source_format = format_of_source_file(source_file) 894 | mkdir_p section_file.pathmap("%d") 895 | normalize_export(export_file, section_file, source_format) 896 | end 897 | end 898 | 899 | file spine_file => [build_dir, *signature_files] do |t| 900 | create_spine_file(t.name, signature_files, stylesheets: stylesheets) 901 | end 902 | 903 | file codex_file => [spine_file, *signature_files] do |t| 904 | create_codex_file(t.name, spine_file) 905 | end 906 | 907 | directory listings_dir 908 | 909 | file skeleton_file => [codex_file, listings_dir] do |t| 910 | create_skeleton_file(t.name, codex_file) 911 | end 912 | 913 | rule /^#{highlights_dir}\/[[:xdigit:]]+\.html$/ => 914 | [->(highlight_file) { listing_for_highlight_file(highlight_file) }] do |t| 915 | dir = t.name.pathmap("%d") 916 | mkdir_p dir unless File.exist?(dir) 917 | sh "pygmentize -o #{t.name} -f html #{t.source}" 918 | end 919 | 920 | file master_file => [skeleton_file, highlights_file] do |t| 921 | create_master_file(t.name, skeleton_file) 922 | end 923 | 924 | file latex_file => [master_file, assets_file] do |t| 925 | mkdir_p t.name.pathmap("%d") 926 | sh pandoc, *pandoc_vars, *%W[--standalone -o #{t.name} #{master_file}] 927 | end 928 | 929 | directory vendor_dir 930 | 931 | file assets_file => master_file do |t| 932 | copy_assets(master_file, master_dir) 933 | touch t.name 934 | end 935 | end 936 | 937 | def define_plugin_tasks 938 | plugins.values.each do |plugin| 939 | plugin.define_tasks 940 | end 941 | end 942 | end 943 | end 944 | -------------------------------------------------------------------------------- /lib/quarto/bundle.rb: -------------------------------------------------------------------------------- 1 | require "quarto/plugin" 2 | 3 | module Quarto 4 | class Bundle < Plugin 5 | def define_tasks 6 | desc "Build a bundle" 7 | task :bundle => bundle_file 8 | 9 | task :deliverables => :bundle 10 | 11 | 12 | file bundle_file => main.deliverable_files do |t| 13 | cd main.deliverable_dir do 14 | sh "zip -r #{t.name.pathmap("%f")} #{main.deliverable_files.pathmap('%f')}" 15 | end 16 | end 17 | end 18 | 19 | private 20 | 21 | def bundle_file 22 | "#{main.deliverable_dir}/#{main.name}.zip" 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/quarto/calibre_mobi.rb: -------------------------------------------------------------------------------- 1 | require "quarto/plugin" 2 | 3 | module Quarto 4 | class CalibreMobi < Plugin 5 | def enhance_build(build) 6 | build.deliverable_files << mobi_file 7 | end 8 | 9 | def define_tasks 10 | desc "Generate a Mobi (Kindle) file from EPUB file" 11 | task :mobi => mobi_file 12 | 13 | file mobi_file => epub_file do 14 | convert_epub_to_mobi(epub_file, mobi_file) 15 | end 16 | end 17 | 18 | def mobi_file 19 | "#{main.deliverable_dir}/#{main.name}.mobi" 20 | end 21 | 22 | def epub_file 23 | main.epub_file 24 | end 25 | 26 | def calibre_flags 27 | %W[--mobi-file-type=both] 28 | end 29 | 30 | def convert_epub_to_mobi(epub_file, mobi_file) 31 | sh "ebook-convert #{epub_file} #{mobi_file} #{calibre_flags.join(' ')}" 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/quarto/doc_raptor.rb: -------------------------------------------------------------------------------- 1 | require "quarto/prince" 2 | require "doc_raptor" 3 | require "netrc" 4 | 5 | module Quarto 6 | class DocRaptor < Prince 7 | private 8 | 9 | MISSING_API_KEY_MESSAGE = 10 | "Please set DOCRAPTOR_API_KEY env var or add docraptor.com to .netrc" 11 | 12 | def generate_pdf_file(pdf_file, master_file) 13 | test_mode = ENV["QUARTO_ENV"] == "production" ? false : true 14 | api_key = ENV.fetch("DOCRAPTOR_API_KEY") { 15 | username, password = Netrc.read["docraptor.com"] 16 | username or fail MISSING_API_KEY_MESSAGE 17 | } 18 | puts "create #{pdf_file} using DocRaptor (test: #{test_mode})" 19 | # global data == yuck :-( 20 | ::DocRaptor.api_key api_key 21 | ::DocRaptor.create( 22 | document_content: File.read(master_file), 23 | name: master_file.pathmap("%f"), 24 | document_type: "pdf", 25 | test: test_mode, 26 | prince_options: {input: "xml"}) do |file, response| 27 | 28 | if response.code.to_i == 200 29 | open(pdf_file, 'w') do |pdf| 30 | IO.copy_stream(file, pdf) 31 | end 32 | else 33 | puts "Error #{response.code}: #{response.message}" 34 | puts file.read 35 | end 36 | end 37 | end 38 | 39 | def prince_dir 40 | "#{main.build_dir}/doc_raptor" 41 | end 42 | 43 | def prince_master_file 44 | "#{main.master_dir}/prince_master.xhtml" 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/quarto/epubcheck.rb: -------------------------------------------------------------------------------- 1 | module Quarto 2 | class Epubcheck < Plugin 3 | fattr(:version) { "3.0.1" } 4 | 5 | def define_tasks 6 | namespace :epubcheck do 7 | desc "Download and prepare epubcheck" 8 | task :vendor => epubcheck_jar 9 | end 10 | 11 | desc "Validate EPUB file(s) with epubcheck" 12 | task :epubcheck => [epubcheck_jar, :epub] do |t| 13 | files = FileList["#{main.deliverable_dir}/*.epub"] 14 | files.each do |epub_file| 15 | sh(*%W[java -jar #{epubcheck_jar} #{epub_file} -v 3.0]) do 16 | # Ignore errors for now 17 | end 18 | end 19 | end 20 | 21 | file epubcheck_jar => epubcheck_package do |t| 22 | cd main.vendor_dir do 23 | sh *%W[unzip #{package_name}] 24 | end 25 | end 26 | 27 | file epubcheck_package do |t| 28 | cd t.name.pathmap("%d") do 29 | sh *%W[wget #{package_url}] 30 | end 31 | end 32 | end 33 | 34 | private 35 | 36 | def epubcheck_jar 37 | "#{main.vendor_dir}/epubcheck-#{version}/epubcheck-#{version}.jar" 38 | end 39 | 40 | def epubcheck_package 41 | "#{main.vendor_dir}/#{package_name}" 42 | end 43 | 44 | def package_name 45 | "epubcheck-#{version}.zip" 46 | end 47 | 48 | def package_url 49 | "https://epubcheck.googlecode.com/files/epubcheck-#{version}.zip" 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/quarto/font.rb: -------------------------------------------------------------------------------- 1 | require "quarto/uri_helpers" 2 | 3 | module Quarto 4 | Font = Struct.new(:family, :weight, :style, :file) do 5 | include UriHelpers 6 | 7 | def initialize(family, options={}) 8 | self.family = family 9 | self.weight = options.delete(:weight) { "normal" } 10 | self.style = options.delete(:style) { "normal" } 11 | self.file = options.delete(:file) { nil } 12 | raise "Unknown options: #{options.inspect}" unless options.empty? 13 | end 14 | 15 | def to_font_face_rule(options={}) 16 | < /dev/null 2>&1") 10 | # See if it is a registered file with git 11 | ls_git = `git ls-files #{file}` 12 | # See if it is an unregistered but un-ignored file 13 | ls_other = 14 | `git ls-files --others --exclude-per-directory .gitignore #{file}` 15 | # If it shows up in neither of the above, exclude it 16 | ls_git.empty? && ls_other.empty? 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/quarto/kindlegen.rb: -------------------------------------------------------------------------------- 1 | require "quarto/plugin" 2 | 3 | module Quarto 4 | class Kindlegen < Plugin 5 | def define_tasks 6 | task :deliverables => kf8_file 7 | 8 | desc "Generate a Kindle file" 9 | task :kindlegen => kf8_file 10 | 11 | directory kindlegen_dir 12 | 13 | file kf8_file => [kindlegen_dir, epub_file] do |t| 14 | sh *%W[kindlegen #{epub_file} -o #{kf8_file.pathmap("%f")}] do 15 | # Ignore warning for now 16 | end 17 | end 18 | end 19 | 20 | private 21 | 22 | def kf8_file 23 | "#{kindlegen_dir}/#{main.name}.kf8" 24 | end 25 | 26 | def epub_file 27 | main.epub_file 28 | end 29 | 30 | def kindlegen_dir 31 | "#{main.deliverable_dir}/kindlegen" 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/quarto/markdown.rb: -------------------------------------------------------------------------------- 1 | require 'quarto' 2 | require 'forwardable' 3 | 4 | module Quarto 5 | class Markdown < Plugin 6 | include Rake::DSL 7 | 8 | module BuildExt 9 | extend Forwardable 10 | 11 | attr_accessor :markdown 12 | 13 | def_delegators :markdown, 14 | :export_from_markdown, 15 | :normalize_markdown_export 16 | end 17 | 18 | def enhance_build(build) 19 | build.extend(BuildExt) 20 | build.markdown = self 21 | build.extensions_to_source_formats["md"] = "markdown" 22 | build.extensions_to_source_formats["markdown"] = "markdown" 23 | build.source_files.include("**/*.md") 24 | build.source_files.include("**/*.markdown") 25 | end 26 | 27 | def export_from_markdown(export_file, source_file) 28 | sh *%W[pandoc --no-highlight -w html5 --standalone 29 | -o #{export_file} #{source_file}] 30 | end 31 | 32 | def normalize_markdown_export(export_file, section_file) 33 | main.normalize_generic_export(export_file, section_file, 34 | before: method(:pre_normalize)) do |doc| 35 | source_listing_pre_elts = doc.css("pre[class]>code").map(&:parent) 36 | source_listing_pre_elts.each do |elt| 37 | elt["class"] = elt["class"] + " sourceCode" 38 | end 39 | end 40 | end 41 | 42 | def pre_normalize(doc) 43 | header_elt = doc.at_css("header") 44 | header_elt.remove if header_elt 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/quarto/orgmode.rb: -------------------------------------------------------------------------------- 1 | require 'quarto' 2 | require 'forwardable' 3 | 4 | module Quarto 5 | class Orgmode < Plugin 6 | ORG_EXPORT_ASYNC = "nil" 7 | ORG_EXPORT_SUBTREE = "nil" 8 | ORG_EXPORT_VISIBLE = "nil" 9 | ORG_EXPORT_BODY_ONLY = "nil" 10 | ORG_EXPORT_ELISP = <>") 15 | (org-mode) 16 | (message (concat "Org version: " org-version)) 17 | (message (concat "CWD: " (pwd))) 18 | (org-html-export-to-html 19 | <%= ORG_EXPORT_ASYNC %> <%= ORG_EXPORT_SUBTREE %> 20 | <%= ORG_EXPORT_VISIBLE %> <%= ORG_EXPORT_BODY_ONLY %> 21 | (quote (<%= orgmode_export_plist %>))) 22 | (kill-emacs)) 23 | END 24 | 25 | module BuildExt 26 | extend Forwardable 27 | 28 | attr_accessor :orgmode 29 | 30 | def_delegators :orgmode, 31 | :export_from_orgmode, 32 | :normalize_orgmode_export 33 | end 34 | 35 | fattr(:emacs_load_path) { 36 | FileList[orgmode_lisp_dir] 37 | } 38 | 39 | def enhance_build(build) 40 | build.extend(Quarto::Orgmode::BuildExt) 41 | build.orgmode = Quarto::Orgmode.new(build) 42 | build.extensions_to_source_formats["org"] = "orgmode" 43 | build.source_files.include("**/*.org") 44 | end 45 | 46 | def define_tasks 47 | namespace :orgmode do 48 | task :vendor => vendor_dir 49 | end 50 | 51 | directory vendor_dir => 52 | "#{main.vendor_dir}/org-#{version}.tar.gz" do |t| 53 | 54 | cd main.vendor_dir do 55 | sh "tar -xzf org-#{version}.tar.gz" 56 | end 57 | cd vendor_dir do 58 | sh "make" 59 | end 60 | end 61 | 62 | file "#{main.vendor_dir}/org-#{version}.tar.gz" => 63 | main.vendor_dir do |t| 64 | cd main.vendor_dir do 65 | sh "wget http://orgmode.org/org-#{version}.tar.gz" 66 | end 67 | end 68 | end 69 | 70 | def finalize_build(build) 71 | task :prepare => "orgmode:vendor" 72 | end 73 | 74 | def version 75 | "8.0.7" 76 | end 77 | 78 | def orgmode_lisp_dir 79 | "#{vendor_dir}/lisp" 80 | end 81 | 82 | def orgmode_export_plist 83 | %W[ 84 | :with-toc nil 85 | :headline-levels 6 86 | :section-numbers nil 87 | :language #{main.language} 88 | :htmlized-source nil 89 | :html-postamble nil 90 | :with-sub-superscript nil 91 | ].join(" ") 92 | end 93 | 94 | def vendor_dir 95 | "#{main.vendor_dir}/org-#{version}" 96 | end 97 | 98 | def export_from_orgmode(export_file, source_file) 99 | language = language 100 | elisp = ERB.new(ORG_EXPORT_ELISP).result(binding) 101 | 102 | sh "emacs", *emacs_flags, *%W[--file #{source_file} --eval #{elisp}] 103 | mv source_file.pathmap("%X.html"), export_file 104 | end 105 | 106 | def emacs_flags 107 | emacs_load_path_flags = emacs_load_path.pathmap("--directory=%p") 108 | ["--batch", *emacs_load_path_flags] 109 | end 110 | 111 | def normalize_orgmode_export(export_file, section_file) 112 | main.normalize_generic_export(export_file, section_file, 113 | before: method(:pre_normalize)) do 114 | |normal_doc| 115 | listing_pre_elts = normal_doc.css("div.org-src-container > pre.src") 116 | listing_pre_elts.each do |elt| 117 | language = elt["class"].split.grep(/^src-(.*)$/) do 118 | break $1 119 | end 120 | elt.parent.replace(normal_doc.create_element("pre") do |pre_elt| 121 | pre_elt["class"] = "sourceCode #{language}" 122 | pre_elt.add_child(normal_doc.create_element("code", elt.text)) 123 | end) 124 | end 125 | figure_elts = normal_doc.css("div.figure") 126 | figure_elts.each do |elt| 127 | img_elt = elt.css("img") 128 | caption_elt = elt.at_css("p:nth-child(2)") 129 | caption = caption_elt && caption_elt.content 130 | elt.replace(normal_doc.create_element("figure") do |fig_elt| 131 | fig_elt.add_child(img_elt) 132 | if caption 133 | fig_elt.add_child(normal_doc.create_element("figcaption", caption)) 134 | end 135 | end) 136 | end 137 | normal_doc.css("div.imprint h2").remove 138 | normal_doc.css("div.dedication h2").remove 139 | promote_headings(normal_doc) 140 | end 141 | end 142 | 143 | def pre_normalize(doc) 144 | content_elt = doc.at_css("div#content") 145 | if content_elt 146 | doc.at_css("body").children = content_elt.children 147 | end 148 | title_h1 = doc.at_css("h1.title") 149 | title_h1.remove if title_h1 150 | end 151 | 152 | def promote_headings(doc) 153 | promotions = { 154 | "h2" => "h1", 155 | "h3" => "h2", 156 | "h4" => "h3", 157 | "h5" => "h4", 158 | "h6" => "h5", 159 | } 160 | 161 | promotions.keys.each do |h_tag| 162 | doc.css("div.outline-2 #{h_tag}, section.chapter #{h_tag}").each do 163 | |heading| 164 | heading.name = promotions[heading.name] 165 | end 166 | end 167 | end 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /lib/quarto/pandoc_epub.rb: -------------------------------------------------------------------------------- 1 | require "quarto/plugin" 2 | require "nokogiri" 3 | require "delegate" 4 | require "quarto/path_helpers" 5 | 6 | module Quarto 7 | class PandocEpub < Plugin 8 | include PathHelpers 9 | module BuildExt 10 | extend Forwardable 11 | 12 | attr_accessor :pandoc_epub 13 | 14 | def epub_file 15 | pandoc_epub.epub_file 16 | end 17 | end 18 | 19 | fattr(:target) { :epub3 } 20 | fattr(:flags) { %W[-w #{target_format} --epub-chapter-level 2 --no-highlight --toc] } 21 | fattr(:xml_write_options) { 22 | Nokogiri::XML::Node::SaveOptions::DEFAULT_XML | 23 | Nokogiri::XML::Node::SaveOptions::NO_DECLARATION 24 | } 25 | 26 | def initialize(*) 27 | super 28 | unless valid_targets.include?(target) 29 | raise "target must be one of: #{valid_targets.join(', ')}" 30 | end 31 | end 32 | 33 | def enhance_build(build) 34 | build.extend(BuildExt) 35 | build.pandoc_epub = self 36 | build.deliverable_files << epub_file 37 | end 38 | 39 | def define_tasks 40 | desc "Build an epub file with pandoc" 41 | task :epub => "pandoc_epub:epub" 42 | 43 | namespace :pandoc_epub do 44 | task :epub => epub_file 45 | end 46 | 47 | # This needs to be dependent on a specific file in the unpacked 48 | # epub because dependencies on directories are unreliable 49 | file epub_file => ["#{exploded_epub}/content.opf"] do |t| 50 | replace_listings(exploded_epub, main.highlights_dir) 51 | fix_font_mimetypes("#{exploded_epub}/content.opf") 52 | add_fallback_styling_classes(exploded_epub) 53 | add_class_to_toc("#{exploded_epub}/nav.xhtml") 54 | add_class_to_cover("#{exploded_epub}/cover.xhtml") 55 | target = Pathname(t.name).relative_path_from(Pathname(exploded_epub)) 56 | cd exploded_epub do 57 | files = FileList["**/*"] 58 | # mimetype file MUST be the first one into the zip file, so 59 | # we handle it separately. 60 | files.exclude("mimetype") 61 | # -X: no extended attributes. These make the EPUB invalid. 62 | sh "zip -X -r #{target} mimetype #{files}" 63 | end 64 | end 65 | 66 | file "#{exploded_epub}/content.opf" => pristine_epub do |t| 67 | rm_rf exploded_epub if File.exist?(exploded_epub) 68 | mkdir_p exploded_epub 69 | sh *%W[unzip #{pristine_epub} -d #{exploded_epub}] 70 | end 71 | 72 | file pristine_epub => [ 73 | *main.all_master_files, 74 | main.deliverable_dir, 75 | main.bitmap_cover_image, 76 | stylesheet, 77 | metadata_file, 78 | *font_files 79 | ].compact do |t| 80 | create_epub_file( 81 | t.name, 82 | main.master_file, 83 | stylesheet: stylesheet, 84 | metadata_file: metadata_file, 85 | font_files: font_files, 86 | cover_image: main.bitmap_cover_image) 87 | end 88 | 89 | file stylesheet => [pandoc_epub_dir, *stylesheets] do |t| 90 | sh "cat #{stylesheets} > #{t.name}" 91 | end 92 | 93 | file fonts_stylesheet do |t| 94 | create_fonts_stylesheet(t.name, fonts) 95 | end 96 | 97 | # In order to set stuff like author, title, etc. Pandoc requires 98 | # a metadata file containing XML Dublin Core properties. Note 99 | # that it doesn't care about proper namespacing. 100 | file metadata_file => main.master_file do |t| 101 | master_doc = open(main.master_file) do |f| 102 | Nokogiri::XML(f) 103 | end 104 | open(t.name, 'w') do |f| 105 | master_doc.css("meta").each do |meta| 106 | if meta["name"] =~ /^DC\.(.*)$/ && meta["content"].size > 0 107 | f.puts "#{meta["content"]}" 108 | end 109 | end 110 | end 111 | end 112 | 113 | rule %r(^#{pandoc_epub_dir}/fonts/.*\.otf$) => [->(f){source_font_for(f)}] do |t| 114 | mkdir_p t.name.pathmap("%d") unless File.exist?(t.name.pathmap("%d")) 115 | convert_font(t.source, t.name) 116 | end 117 | 118 | directory pandoc_epub_dir 119 | end 120 | 121 | # The final product 122 | def epub_file 123 | "#{main.deliverable_dir}/#{main.name}.epub" 124 | end 125 | 126 | private 127 | 128 | def create_epub_file(epub_file, master_file, options={}) 129 | metadata_file = options.fetch(:metadata_file) { self.metadata_file } 130 | font_files = options.fetch(:font_files) { [] } 131 | pandoc_flags = flags.dup 132 | master_dir = master_file.pathmap("%d") 133 | epub_file = rel_path(epub_file, master_dir) 134 | metadata_path = rel_path(metadata_file, master_dir) 135 | if stylesheet_file = options[:stylesheet] 136 | stylesheet_file = rel_path(stylesheet_file, master_dir) 137 | pandoc_flags.concat(%W[--epub-stylesheet #{stylesheet_file}]) 138 | end 139 | if cover_image = options[:cover_image] 140 | pandoc_flags.concat( 141 | %W[--epub-cover-image #{rel_path(cover_image, master_dir)}]) 142 | end 143 | font_files.each do |font_file| 144 | font_path = rel_path(font_file, master_dir) 145 | pandoc_flags.concat(%W[--epub-embed-font #{font_path}]) 146 | end 147 | pandoc_flags.concat(%W[--epub-metadata #{metadata_path}]) 148 | cd master_dir do 149 | sh pandoc, "-o", epub_file, master_file.pathmap("%f"), *pandoc_flags 150 | end 151 | end 152 | 153 | # Pandoc eats all tags inside
 tags, leaving only the
154 |     # text. (See https://github.com/jgm/pandoc/issues/221). We have to
155 |     # look up the highlighted listing by SHA1 and replace the
156 |     # Pandoc-mangled listing with the original.
157 |     def replace_listings(epub_dir, highlights_dir)
158 |       files = FileList["#{epub_dir}/**/*.xhtml"]
159 |       files.each do |file|
160 |         doc = open(file) { |f|
161 |           Nokogiri::XML(f)
162 |         }
163 |         listing_elts = doc.css("pre > code")
164 |         if listing_elts.empty?
165 |           puts "no listings in #{file}"
166 |           next
167 |         else
168 |           puts "replace listings in #{file}"
169 |         end
170 |         listing_elts.each_with_index do |listing_elt, index|
171 |           listing_elt = listing_elt.parent
172 |           code = listing_elt.text
173 |           sha1s = [Digest::SHA1.hexdigest(code), Digest::SHA1.hexdigest(code + "\n")]
174 |           highlight_file = sha1s.map { |sha1|
175 |             "#{highlights_dir}/#{sha1}.html"
176 |           }.detect { |hf|
177 |             File.exist?(hf)
178 |           }
179 |           if highlight_file
180 |             puts "  replace listing ##{index + 1} with #{highlight_file}"
181 |             listing_elt.replace(File.read(highlight_file))
182 |           else
183 |             puts "  no highlight found for listing ##{index + 1}"
184 |             puts "----"
185 |             puts listing_elt.text
186 |             puts "----"
187 |           end
188 |         end
189 |         open(file, 'w') do |f|
190 |           doc.write_xml_to(f, save_with: xml_write_options)
191 |         end
192 |       end
193 |     end
194 | 
195 |     def add_fallback_styling_classes(epub_dir)
196 |       files = FileList["#{epub_dir}/**/*.xhtml"]
197 |       files.each do |file|
198 |         doc = open(file) { |f|
199 |           Nokogiri::XML(f)
200 |         }
201 |         query = (1..6).map{|n| "h#{n} + p"}.join(", ")
202 |         doc.css(query).each do |elt|
203 |           heading_name = elt.previous_element.name
204 |           classes = %W[first-para first-para-after-#{heading_name}]
205 |           elt["class"] = (elt["class"].to_s.split + classes).join(" ")
206 |         end
207 |         # Why doesn't pandoc add "type" attributes to stylesheet link
208 |         # tags when generating EPUB3? Who the fuck knows.
209 |         # TODO: Move this into its own method, it is unrelated
210 |         doc.css("link[rel='stylesheet'][href$='.css']").each do |elt|
211 |           elt["type"] = "text/css"
212 |         end
213 |         open(file, 'w') do |f|
214 |           doc.write_xml_to(f, save_with: xml_write_options)
215 |         end
216 |       end
217 |     end
218 | 
219 |     def create_fonts_stylesheet(file, fonts)
220 |       puts "generate #{file}"
221 |       open(file, 'w') do |f|
222 |         fonts.each do |font|
223 |           f.puts(font.to_font_face_rule(basename: true))
224 |         end
225 |       end
226 |     end
227 | 
228 |     # Replace Pandoc mimetypes with the ones recognized by the IDPF.
229 |     # TODO Maybe refactor this to use MIME::Types if it reurns
230 |     #      IDPF-compliant types.
231 |     def fix_font_mimetypes(package_file)
232 |       doc = open(package_file) {|f|
233 |         Nokogiri::XML(f)
234 |       }
235 |       doc.css("manifest item[media-type='application/x-font-woff']").each do |elt|
236 |         elt["media-type"] = "application/font-woff"
237 |       end
238 |       doc.css("manifest item[media-type='application/x-font-opentype']").each do
239 |         |elt|
240 |         elt["media-type"] = "application/vnd.ms-opentype"
241 |       end
242 |       open(package_file, 'w') do |f|
243 |         doc.write_xml_to(f)
244 |       end
245 |     end
246 | 
247 |     # Styling the table of contents is a nightmare. A typical EPUB3
248 |     # TOC is contained in a nav element like this: . I've had limited success using either
250 |     # namespaced (CSS3) attribute value selectors
251 |     # (nav[epub|type='toc']) or escaped attribute value selectors
252 |     # (nav[epub\:type='toc']). But in theory even the dumbest CSS
253 |     # parser should support a simple ID attribute. So here we add an
254 |     # ID as a lowest-common-denominator way to find and style the TOC.
255 |     def add_class_to_toc(nav_file)
256 |       doc = open(nav_file) {|f| Nokogiri::XML(f)}
257 |       doc.at_css("nav")["id"] = "TOC"
258 |       open(nav_file, 'w') do |f|
259 |         doc.write_xml_to(f, save_with: xml_write_options)
260 |       end
261 |     end
262 | 
263 |     def add_class_to_cover(cover_file)
264 |       if !File.exist?(cover_file)
265 |         warn "Cover file not found: #{cover_file}"
266 |         return
267 |       end
268 |       doc = open(cover_file) {|f| Nokogiri::XML(f)}
269 |       doc.at_css("#cover-image")["class"] = "frontcover"
270 |       open(cover_file, 'w') do |f|
271 |         doc.write_xml_to(f, save_with: xml_write_options)
272 |       end
273 |     end
274 | 
275 |     def stylesheet
276 |       "#{pandoc_epub_dir}/stylesheet.css"
277 |     end
278 | 
279 |     # The directory into which we unpack the pristine_epub so that we
280 |     # can fix it up.
281 |     def exploded_epub
282 |       "#{pandoc_epub_dir}/book"
283 |     end
284 | 
285 |     # The pristine EPUB file is the one that Pandoc produces, before
286 |     # we unpack it and do various fix-ups to it.
287 |     def pristine_epub
288 |       "#{pandoc_epub_dir}/book.epub"
289 |     end
290 | 
291 |     def pandoc_epub_dir
292 |       "#{main.build_dir}/pandoc_epub"
293 |     end
294 | 
295 |     def pandoc
296 |       main.pandoc
297 |     end
298 | 
299 |     def fonts_stylesheet
300 |       "#{pandoc_epub_dir}/fonts.css"
301 |     end
302 | 
303 |     def metadata_file
304 |       "#{pandoc_epub_dir}/metadata.xml"
305 |     end
306 | 
307 |     def stylesheets
308 |       main.stylesheets.applicable_to(target).master_files + [fonts_stylesheet]
309 |     end
310 | 
311 |     def valid_targets
312 |       [:epub2, :epub3]
313 |     end
314 | 
315 |     def target_format
316 |       case target
317 |       when :epub2 then "epub"
318 |       when :epub3 then "epub3"
319 |       else raise "Unknown target #{target}"
320 |       end
321 |     end
322 | 
323 |     # Return a list of font file paths where any non-EPUB3-standard
324 |     # font extensions are replaced with ".otf". See also
325 |     # #convert_font.
326 |     def font_files
327 |       orig_files = FileList[*main.fonts.map(&:file)]
328 |       supported, unsupported = orig_files.partition{|f|
329 |         %W[.otf].include?(f.pathmap("%x"))
330 |       }
331 |       (supported + unsupported.pathmap("#{pandoc_epub_dir}/fonts/%n.otf"))
332 |     end
333 | 
334 |     def source_font_for(target_font)
335 |       orig_files = main.fonts.map(&:file)
336 |       orig_files.detect{|f| f.pathmap("%n") == target_font.pathmap("%n")}
337 |     end
338 | 
339 |     # While some (many?) readers support TrueType, SVG, etc. fonts,
340 |     # EPUB3 only requires support for WOFF and OpenType. This method
341 |     # uses FontForge (http://fontforge.org/) to convert from arbitrary
342 |     # font types to OTF.
343 |     #
344 |     # Note that while EPUB3 supports both WOFF and OpenType, KF8
345 |     # supports only TrueType and OpenType. The only common format is
346 |     # OpenType, and since EPUBs may be used as the source format for
347 |     # KF8, that's what we target.
348 |     def convert_font(source_font, target_font)
349 |       convert_script = File.expand_path("../../../fontforge/convert.pe", __FILE__)
350 |       sh "fontforge", "-script", convert_script, source_font, target_font
351 |     end
352 | 
353 |     def font_file_for_epub(orig_file)
354 |       font_files.detect{|f| orig_file.pathmap("%n") == f.pathmap("%n")}
355 |     end
356 | 
357 |     def fonts
358 |       main.fonts.map{|font|
359 |         if %W[.otf].include?(font.file.pathmap("%x"))
360 |           font
361 |         else
362 |           font = font.dup
363 |           font.file = font_file_for_epub(font.file)
364 |           font
365 |         end
366 |       }
367 |     end
368 | 
369 |   end
370 | end
371 | 


--------------------------------------------------------------------------------
/lib/quarto/path_helpers.rb:
--------------------------------------------------------------------------------
 1 | module Quarto
 2 |   module PathHelpers
 3 |     module_function
 4 | 
 5 |     def rel_path(file, dir)
 6 |       Pathname(file).relative_path_from(Pathname(dir)).to_s
 7 |     end
 8 | 
 9 |     def clean_path(path)
10 |       Pathname(path).cleanpath.to_s
11 |     end
12 |   end
13 | end
14 | 


--------------------------------------------------------------------------------
/lib/quarto/pdf_samples.rb:
--------------------------------------------------------------------------------
 1 | require "quarto/plugin"
 2 | require "forwardable"
 3 | 
 4 | module Quarto
 5 |   # The PdfSamples plugin enables users to generate sample PDFs that
 6 |   # are based on selected pages from the final PDF deliverable.
 7 |   class PdfSamples < Plugin
 8 |     module BuildExt
 9 |       fattr(:pdf_samples)
10 |       def add_pdf_sample(**options)
11 |         pdf_samples.add(**options)
12 |       end
13 |     end
14 | 
15 |     SampleDef = Struct.new(:name, :selections, :description)
16 | 
17 |     def enhance_build(build)
18 |       build.extend(BuildExt)
19 |       build.pdf_samples = self
20 |     end
21 | 
22 |     def define_tasks
23 |       task :deliverables => :pdf_samples
24 | 
25 |       desc "Generate PDF sample files from the PDF book file"
26 |       task :pdf_samples => pdf_sample_files
27 | 
28 |       directory sample_dir
29 | 
30 |       sample_defs.each do |sample|
31 |         file sample_path(sample.name) => [standalone_pdf_file, sample_dir] do
32 |           extract_sample(standalone_pdf_file, sample_path(sample.name), *sample.selections)
33 |         end
34 |       end
35 |     end
36 | 
37 |     def standalone_pdf_file
38 |       main.standalone_pdf_file
39 |     end
40 | 
41 |     def sample_defs
42 |       @samples ||= []
43 |     end
44 | 
45 |     def add(name:, select:, desc: "")
46 |       sample_def = SampleDef.new(name, select, desc)
47 |       sample_defs << sample_def
48 |     end
49 | 
50 |     def extract_sample(source_pdf, sample_path, *selections)
51 |       selection_flags = pdftk_selection_flags("A", *selections)
52 |       sh "pdftk A=#{source_pdf} cat #{selection_flags} output #{sample_path}"
53 |     end
54 | 
55 |     def pdftk_selection_flags(handle, *selections)
56 |       flags = selections.map { |selection|
57 |         case selection
58 |         when Integer then selection.to_s
59 |         when Range
60 |           "#{selection.first.to_i}-#{selection.last.to_i}"
61 |         end
62 |       }
63 |       flags.map{|flag| "#{handle}#{flag}"}.join(" ")
64 |     end
65 | 
66 |     def pdf_sample_files
67 |       sample_defs.map{|sample| sample_path(sample.name)}
68 |     end
69 | 
70 |     def sample_path(sample_name)
71 |       "#{sample_dir}/#{sample_name}.pdf"
72 |     end
73 | 
74 |     def sample_dir
75 |       "#{main.build_dir}/pdf_samples"
76 |     end
77 |   end
78 | end
79 | 


--------------------------------------------------------------------------------
/lib/quarto/plugin.rb:
--------------------------------------------------------------------------------
 1 | require "rake"
 2 | require "fattr"
 3 | 
 4 | module Quarto
 5 |   class Plugin
 6 |     include Rake::DSL
 7 | 
 8 |     attr_reader :main
 9 | 
10 |     def initialize(main, options={})
11 |       @main = main
12 |       options.each do |name, value|
13 |         public_send(name, value)
14 |       end
15 |     end
16 | 
17 |     def enhance_build(build)
18 |       # placeholder
19 |     end
20 | 
21 |     def finalize_build(build)
22 |       # placeholder
23 |     end
24 | 
25 |     def define_tasks
26 |       # placeholder
27 |     end
28 | 
29 |     def say(*messages)
30 |       main.say(*messages)
31 |     end
32 |   end
33 | end
34 | 


--------------------------------------------------------------------------------
/lib/quarto/prince.rb:
--------------------------------------------------------------------------------
  1 | require "quarto/plugin"
  2 | require "quarto/uri_helpers"
  3 | require "mime/types"
  4 | require "uri"
  5 | 
  6 | module Quarto
  7 |   class Prince < Plugin
  8 |     include UriHelpers
  9 | 
 10 |     module BuildExt
 11 |       fattr(:prince)
 12 |       def standalone_pdf_file
 13 |         prince.standalone_pdf_file
 14 |       end
 15 |     end
 16 | 
 17 |     fattr(:cover_image)
 18 |     fattr(:xml_write_options) {
 19 |       Nokogiri::XML::Node::SaveOptions::DEFAULT_XML |
 20 |       Nokogiri::XML::Node::SaveOptions::NO_DECLARATION
 21 |     }
 22 | 
 23 |     def enhance_build(build)
 24 |       build.deliverable_files << standalone_pdf_file
 25 |       build.extend(BuildExt)
 26 |       build.prince = self
 27 |     end
 28 | 
 29 |     def define_tasks
 30 |       task :deliverables => :pdf
 31 | 
 32 |       desc "Build a PDF with PrinceXML"
 33 |       task :pdf => :"prince:pdf"
 34 | 
 35 |       namespace :prince do
 36 |         task :pdf => pdf_files
 37 |       end
 38 | 
 39 |       file standalone_pdf_file => [prince_master_file] do |t|
 40 |         mkdir_p t.name.pathmap("%d")
 41 |         generate_pdf_file(standalone_pdf_file, prince_master_file)
 42 |       end
 43 | 
 44 |       file interior_pdf_file => [prince_interior_master_file] do |t|
 45 |         mkdir_p t.name.pathmap("%d")
 46 |         generate_pdf_file(interior_pdf_file, prince_interior_master_file)
 47 |       end
 48 | 
 49 |       file prince_master_file => [main.master_file, main.assets_file, toc_file, stylesheet] do |t|
 50 |         create_prince_master_file(prince_master_file,main.master_file, stylesheet, cover: true)
 51 |       end
 52 | 
 53 |       file prince_interior_master_file =>
 54 |         [main.master_file, main.assets_file, toc_file, stylesheet] do |t|
 55 |         create_prince_master_file(prince_interior_master_file, main.master_file, stylesheet, cover: false)
 56 |       end
 57 | 
 58 |       file toc_file => [main.master_file, prince_dir] do |t|
 59 |         generate_cmd = "pandoc --table-of-contents --standalone #{main.master_file}"
 60 |         toc_xpath    = "//*[@id='TOC']"
 61 |         extract_cmd  = %Q(xmlstarlet sel -I -t -c "#{toc_xpath}")
 62 |         sh "#{generate_cmd} | #{extract_cmd} > #{t.name}"
 63 |       end
 64 | 
 65 |       file stylesheet => pdf_stylesheets do |t|
 66 |         sh "cat #{pdf_stylesheets} > #{t.name}"
 67 |       end
 68 | 
 69 |       file font_stylesheet do |t|
 70 |         puts "generate #{t.name}"
 71 |         open(t.name, 'w') do |f|
 72 |           main.fonts.each do |font|
 73 |             f.puts(font.to_font_face_rule(embed: true))
 74 |           end
 75 |         end
 76 |       end
 77 | 
 78 |       directory prince_dir
 79 |     end
 80 | 
 81 |     def pdf_files
 82 |       [standalone_pdf_file, interior_pdf_file]
 83 |     end
 84 | 
 85 |     def standalone_pdf_file
 86 |       "#{main.deliverable_dir}/#{main.name}.pdf"
 87 |     end
 88 | 
 89 |     def interior_pdf_file
 90 |       "#{main.deliverable_dir}/#{main.name}-interior.pdf"
 91 |     end
 92 | 
 93 |     def prince_master_file
 94 |       "#{main.master_dir}/prince_master.xhtml"
 95 |     end
 96 | 
 97 |     def prince_interior_master_file
 98 |       "#{main.master_dir}/prince_interior_master.xhtml"
 99 |     end
100 | 
101 |     def toc_file
102 |       "#{prince_dir}/toc.xml"
103 |     end
104 | 
105 |     def stylesheet
106 |       "#{prince_dir}/styles.css"
107 |     end
108 | 
109 |     def pdf_stylesheets
110 |       main.stylesheets.applicable_to(:pdf).master_files + FileList[font_stylesheet]
111 |     end
112 | 
113 |     def font_stylesheet
114 |       "#{prince_dir}/fonts.css"
115 |     end
116 | 
117 |     def prince_dir
118 |       "#{main.build_dir}/prince"
119 |     end
120 | 
121 |     def create_prince_master_file(prince_master_file, master_file, stylesheet, options={})
122 |       puts "create #{prince_master_file} from #{master_file}"
123 |       doc = open(master_file) { |f|
124 |         Nokogiri::XML(f)
125 |       }
126 |       body_elt = doc.root.at_css("body")
127 |       first_child = body_elt.first_element_child
128 |       if options.fetch(:cover){true} && main.bitmap_cover_image
129 |         cover_image_uri = data_uri_for_file(main.bitmap_cover_image)
130 |         first_child.before(
131 |           "
") 132 | end 133 | first_child.before(File.read(toc_file)) 134 | doc.at_css("#TOC").first_element_child.before("

Table of Contents

") 135 | doc.css("link[rel='stylesheet']").remove 136 | doc.at_css("head").add_child( 137 | doc.create_element("style") do |elt| 138 | elt["type"] = "text/css" 139 | elt.content = File.read(stylesheet) 140 | end) 141 | embed_images(doc) 142 | open(prince_master_file, 'w') do |f| 143 | doc.write_xml_to(f, save_with: xml_write_options) 144 | end 145 | end 146 | 147 | def embed_images(doc) 148 | doc.css("img").each do |elt| 149 | uri = URI.parse(elt["src"]) 150 | if !uri.scheme && File.exist?(uri.path) 151 | elt["src"] = data_uri_for_file(uri.path) 152 | end 153 | end 154 | end 155 | 156 | def generate_pdf_file(pdf_file, master_file) 157 | sh *%W[prince #{master_file} -o #{pdf_file}] 158 | end 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /lib/quarto/site.rb: -------------------------------------------------------------------------------- 1 | require "quarto/plugin" 2 | require "quarto/path_helpers" 3 | require "quarto/template" 4 | require "tilt" 5 | require "forwardable" 6 | 7 | module Quarto 8 | class Site < Plugin 9 | include PathHelpers 10 | extend Forwardable 11 | 12 | module BuildExt 13 | attr_accessor :site 14 | end 15 | 16 | attr_reader :resources 17 | 18 | def initialize(*) 19 | super 20 | @resources = [] 21 | add_resource "index.html" 22 | end 23 | 24 | def enhance_build(build) 25 | build.require_plugin(:template_set) 26 | build.require_plugin(:bower) 27 | build.extend(BuildExt) 28 | build.site = self 29 | end 30 | 31 | def define_tasks 32 | task :default => :site 33 | 34 | desc "Build a website for the book" 35 | task :site => "site:build" 36 | 37 | namespace :site do 38 | task :build => ["fascicles", site_dir, "site:resources", "bower:install"] 39 | 40 | desc "Deploy the book website" 41 | task :deploy => :build 42 | 43 | desc "Start a simple server to test the site" 44 | task :serve do 45 | sh RUBY, *%W[-run -e httpd #{site_dir} -p 9090] 46 | end 47 | 48 | directory site_dir 49 | directory site_template_dir 50 | 51 | task "resources" do 52 | layout_file = 53 | templates.find_template_for("#{main.build_dir}/#{default_layout}") 54 | 55 | resources.each do |resource| 56 | task = Rake.application[resource] 57 | task.enhance([layout_file]) 58 | task.invoke 59 | end 60 | 61 | main.fascicles.each do |fascicle| 62 | site_path = site_fascicle_path(fascicle) 63 | deps = [fascicle.path, fascicle_template_path] 64 | task = Rake.application.define_task(Rake::FileTask, site_path => deps) do 65 | generate_fascicle_page(fascicle, site_path) 66 | end 67 | task.enhance([layout_file]) 68 | task.invoke 69 | end 70 | end 71 | end 72 | 73 | end 74 | 75 | def page_list 76 | "#{main.build_dir}/site-page-list.rake" 77 | end 78 | 79 | def add_resource(resource) 80 | resources << "#{site_dir}/#{resource}" 81 | end 82 | 83 | def fascicle_template_path 84 | templates.find_template_for("#{site_dir}/_fascicle.html") 85 | end 86 | 87 | def find_fascicle_file_for(path) 88 | numbered_name = path.pathmap("%f") 89 | FileList["#{main.fascicle_dir}/#{numbered_name}.xhtml"].first or 90 | fail "Unable to find fascicle corresponding to #{path}" 91 | end 92 | 93 | def default_layout 94 | "site/_layout.html" 95 | end 96 | 97 | def site_dir 98 | "#{main.build_dir}/site" 99 | end 100 | 101 | fattr(:site_template_dir) { "#{templates.template_dir}/site" } 102 | 103 | def site_fascicle_dir 104 | "#{site_dir}/fascicles" 105 | end 106 | 107 | def site_fascicle_path(fascicle) 108 | "#{site_fascicle_dir}/#{fascicle.numbered_name}.html" 109 | end 110 | 111 | def fascicle_url(fascicle) 112 | "/fascicles/#{fascicle.numbered_name}.html" 113 | end 114 | 115 | def generate_fascicle_page(fascicle, path) 116 | fasc_doc = open(fascicle.path) do |f| 117 | Nokogiri::XML(f) 118 | end 119 | content = fasc_doc.at_css("div.signature").children 120 | # TODO: Dingdingding feature envy!!! 121 | template = templates.make_template(fascicle_template_path) 122 | templates.expand_template(template, path, 123 | fascicle: fascicle, 124 | layout: default_layout) do 125 | content.to_html 126 | end 127 | end 128 | 129 | private 130 | 131 | # @!method templates 132 | # @return [TemplateSet] 133 | def_delegators :main, :templates 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /lib/quarto/stylesheet.rb: -------------------------------------------------------------------------------- 1 | require "quarto/path_helpers" 2 | require "quarto/uri_helpers" 3 | require "base64" 4 | require "naught" 5 | 6 | # This class manages stylesheets. 7 | # 8 | # In a perfect world, we'd have one big stylesheet and all the 9 | # variances between devices would be managed with media 10 | # queries. Meanwhile, back on earth, there are three problems with 11 | # this strategy: 12 | # 1. Many devices don't understand media queries. Whether this means 13 | # they ignore the rules within @media {...} blocks or apply ALL of 14 | # them is a big question mark. Other devices only partially support 15 | # media queries. 16 | # 2. Even if they did all understand media queries, there aren't 17 | # specific enough queries to account for the diversity of devices 18 | # out there. There's no media query (that I know of) for "is this 19 | # an e-ink device?", let alone "is this Apple iBooks?". 20 | # 3. Sometimes the mere presence of an unsupported CSS syntax will 21 | # throw software for a loop, or generate warnings. E.g. kindlegen 22 | # spits out warnings for every selector it doesn't happen to 23 | # support. And I've seen PrinceXML refuse to recognize an entire 24 | # rule because ONE of the comma-separated selectors in the rule 25 | # contained a namespaced attribute... even though some of the other 26 | # comma-separated selectors for the same rule were known to be 27 | # recognizable by PrinceXML. 28 | # 29 | # The upshot of all this is that for the foreseeable future we're 30 | # still stuck with generating target-specific stylesheets. That's 31 | # what this class helps with. 32 | module Quarto 33 | class Stylesheet 34 | include PathHelpers 35 | include UriHelpers 36 | 37 | fattr(:stylesheets) 38 | fattr(:path) 39 | fattr(:template_file) { 40 | on_fail = ->{ raise "None found: #{potential_templates}" } 41 | potential_templates.detect(on_fail){|path| File.exist?(path)} 42 | } 43 | fattr(:source_file) { template_file.pathmap("#{source_dir}/%f") } 44 | fattr(:master_file) { "#{master_dir}/#{path}" } 45 | fattr(:source_dir) { stylesheets.source_dir } 46 | fattr(:templates_dir) { stylesheets.templates_dir } 47 | fattr(:master_dir) { stylesheets.master_dir } 48 | fattr(:targets) { [:all] } 49 | 50 | def initialize(stylesheets, path, options={}) 51 | self.stylesheets = stylesheets 52 | self.path = path.ext(".css") 53 | options.each do |key, value| 54 | public_send(key, value) 55 | end 56 | assert_template_file_exists 57 | end 58 | 59 | def link_tag 60 | "" 61 | end 62 | 63 | def data_uri 64 | data_uri_for_file(master_file, "text/css") 65 | end 66 | 67 | def applicable_to_targets?(*targets) 68 | self.targets.include?(:all) || (targets & self.targets).any? 69 | end 70 | 71 | def open(&block) 72 | open(master_file, &block) 73 | end 74 | 75 | def to_s 76 | master_file 77 | end 78 | 79 | def to_path 80 | master_file 81 | end 82 | 83 | def relative_master_file(start_path=stylesheets.main_master_dir) 84 | rel_path(master_file, start_path) 85 | end 86 | 87 | def potential_templates 88 | FileList[ 89 | path.ext(".scss"), 90 | path, 91 | "#{templates_dir}/#{path.ext(".scss")}", 92 | ] 93 | end 94 | 95 | def assert_template_file_exists 96 | raise "#{template_file} does not exist" unless File.exist?(template_file) 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/quarto/stylesheet_set.rb: -------------------------------------------------------------------------------- 1 | require "quarto/plugin" 2 | require "quarto/stylesheet" 3 | 4 | module Quarto 5 | class StylesheetSet < Plugin 6 | include Enumerable 7 | 8 | module BuildExt 9 | fattr(:stylesheets) 10 | def clear_stylesheets 11 | stylesheets.remove_masters_from(all_master_files) 12 | stylesheets.clear 13 | end 14 | end 15 | 16 | NullStylesheet = Naught.build do |config| 17 | config.mimic Stylesheet 18 | end 19 | 20 | fattr(:font) { 'serif' } 21 | fattr(:heading_font) { '"PT Sans", sans-serif' } 22 | fattr(:heading_color) { "black" } 23 | fattr(:left_slug) { nil } 24 | fattr(:right_slug) { nil } 25 | fattr(:print_page_width) { "7.5in" } 26 | fattr(:print_page_height) { "9in" } 27 | fattr(:cover_color) { "black" } 28 | 29 | def initialize(build, sheets=[]) 30 | super(build) 31 | @sheets = sheets 32 | end 33 | 34 | def enhance_build(build) 35 | add("base.css") 36 | add("code.css") 37 | add("pages.css", targets: [:pdf]) 38 | add("pdf.css", targets: [:pdf]) 39 | add("epub2.css", targets: [:epub2]) 40 | add("epub3.css", targets: [:epub3]) 41 | build.extend(BuildExt) 42 | build.stylesheets = self 43 | end 44 | 45 | def finalize_build(build) 46 | build.all_master_files.include(master_files) 47 | end 48 | 49 | def each(&block) 50 | @sheets.each(&block) 51 | end 52 | 53 | def define_tasks 54 | file main.master_file => master_files 55 | 56 | namespace :stylesheets do 57 | task :sources => source_files 58 | task :masters => master_files 59 | end 60 | 61 | rule %r(\A#{source_dir}/.*\.s?css\z) => [ 62 | ->(f){template_for_source(f)}, 63 | source_dir, 64 | var_file 65 | ] do |t| 66 | sh "cat #{var_file} #{t.source} > #{t.name}" 67 | end 68 | 69 | rule %r(\A#{master_dir}/.*\.css\z) => [ 70 | ->(f){source_for_master(f)}, 71 | master_dir, 72 | ] do |t| 73 | if t.source.end_with?(".scss") 74 | sh "sass", *sass_load_paths.pathmap("-I%p"), 75 | *%W[--scss #{t.source} #{t.name}] 76 | else 77 | cp t.source, t.name 78 | end 79 | end 80 | 81 | file var_file => main.build_dir do |t| 82 | say "write #{t.name}" 83 | open(t.name, 'w') do |f| 84 | write_scss_variables(f, variables) 85 | end 86 | end 87 | 88 | directory source_dir 89 | directory master_dir 90 | end 91 | 92 | def add(*args) 93 | @sheets << Stylesheet.new(self, *args) 94 | end 95 | 96 | def clear 97 | @sheets.clear 98 | end 99 | 100 | def remove_masters_from(list) 101 | master_files.each do |mf| 102 | list.delete(mf) 103 | end 104 | end 105 | 106 | def applicable_to(*targets) 107 | sheets = @sheets.select{|s| s.applicable_to_targets?(*targets)} 108 | self.class.new(main, sheets) 109 | end 110 | 111 | def generate_stylesheet_for_targets(io, *targets) 112 | applicable_to(*targets).each do |sheet| 113 | sheet.open do |f| 114 | IO.copy_stream(f, io) 115 | end 116 | end 117 | end 118 | 119 | def write_scss_variables(out, variables) 120 | out.puts "// BEGIN AUTO VARIABLES" 121 | out.puts scss_variable_assignments(variables) 122 | out.puts "// END AUTO VARIABLES" 123 | end 124 | 125 | def scss_variable_assignments(variables) 126 | variables.each_with_object("") do |(name, value), s| 127 | value = value.nil? ? "null" : value 128 | s << "$#{name}: #{value};\n" 129 | end 130 | end 131 | 132 | def variables 133 | { 134 | font: font, 135 | heading_font: heading_font, 136 | heading_color: heading_color, 137 | title: %Q("#{main.title}"), 138 | lslug: left_slug, 139 | rslug: right_slug, 140 | print_page_width: print_page_width, 141 | print_page_height: print_page_height, 142 | vector_cover_image: "url(#{main.vector_cover_image})", 143 | cover_color: cover_color, 144 | } 145 | end 146 | 147 | def template_files 148 | FileList[*@sheets.map(&:master_file)] 149 | end 150 | 151 | def source_files 152 | FileList[*@sheets.map(&:source_file)] 153 | end 154 | 155 | def master_files 156 | FileList[*@sheets.map(&:master_file)] 157 | end 158 | 159 | def template_for_source(source_file) 160 | @sheets.detect(NullStylesheet.method(:new)){ 161 | |s| s.source_file == source_file 162 | }.template_file 163 | end 164 | 165 | def source_for_master(master_file) 166 | @sheets.detect(NullStylesheet.method(:new)){ 167 | |s| s.master_file == master_file 168 | }.source_file 169 | end 170 | 171 | def var_file 172 | "#{main.build_dir}/vars.scss" 173 | end 174 | 175 | def source_dir 176 | "#{main.build_dir}/stylesheets" 177 | end 178 | 179 | def templates_dir 180 | File.expand_path("../../../templates/stylesheets", __FILE__) 181 | end 182 | 183 | def master_dir 184 | "#{main_master_dir}/stylesheets" 185 | end 186 | 187 | def main_master_dir 188 | main.master_dir 189 | end 190 | 191 | def sass_load_paths 192 | FileList[templates_dir] 193 | end 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /lib/quarto/tasks.rb: -------------------------------------------------------------------------------- 1 | require 'quarto' 2 | Quarto.configure do |config| 3 | config.use :orgmode 4 | config.use :markdown 5 | config.use :pandoc_epub 6 | config.use :epubcheck 7 | end 8 | -------------------------------------------------------------------------------- /lib/quarto/template.rb: -------------------------------------------------------------------------------- 1 | require "quarto/path_helpers" 2 | require "tilt" 3 | require "slim" 4 | 5 | Tilt.register(Slim::Template, "slim") 6 | 7 | module Quarto 8 | class Template 9 | RenderContext = Struct.new(:build, :layout, :root_dir) do 10 | def render(template, **locals, &block) 11 | path = "#{root_dir}/#{template}" 12 | template = Template.new(build.templates.find_template_for(path), build) 13 | template.render(build, 14 | root_dir: root_dir, 15 | render_context: self, 16 | **locals, 17 | &block) 18 | end 19 | end 20 | 21 | include PathHelpers 22 | 23 | attr_reader :path 24 | 25 | def initialize(path, build) 26 | @path = path 27 | @build = build 28 | end 29 | 30 | def to_path 31 | path 32 | end 33 | 34 | def to_str 35 | path 36 | end 37 | 38 | def to_s 39 | "#<#{self.class}:#{path}>" 40 | end 41 | 42 | def final? 43 | tilt_template = Tilt[path] 44 | tilt_template.nil? || tilt_template == Tilt::PlainTemplate 45 | end 46 | 47 | def html? 48 | path.pathmap("%f") =~ /\.html\b/ 49 | end 50 | 51 | def render( 52 | build, 53 | layout: nil, 54 | root_dir: build.build_dir, 55 | render_context: RenderContext.new(build, layout, root_dir), 56 | **locals, 57 | &block) 58 | tilt_template = Tilt.new(path, **tilt_template_options(path)) 59 | content = tilt_template.render(render_context, locals, &block) 60 | if layout 61 | layout.render(build, 62 | root_dir: root_dir, 63 | render_context: render_context, 64 | **locals) do 65 | content 66 | end 67 | else 68 | content 69 | end 70 | end 71 | 72 | def tilt_template_options(path) 73 | case path.pathmap("%x") 74 | when ".slim" then {format: :html5, pretty: true} 75 | when ".scss" then scss_options 76 | else {} 77 | end 78 | end 79 | 80 | def scss_options 81 | { 82 | load_paths: [@build.stylesheets.templates_dir] 83 | } 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/quarto/template_set.rb: -------------------------------------------------------------------------------- 1 | require "quarto/plugin" 2 | require "quarto/template" 3 | require "pathname" 4 | 5 | module Quarto 6 | class TemplateSet < Plugin 7 | include PathHelpers 8 | 9 | module BuildExt 10 | attr_accessor :templates 11 | end 12 | 13 | UNMET_DEPENDENCY = ["<>"] 14 | 15 | def enhance_build(build) 16 | build.extend(BuildExt) 17 | build.templates = self 18 | end 19 | 20 | def define_tasks 21 | rule %r(#{main.build_dir}/.*) => method(:find_template_deps_for) do |t| 22 | generate_file_from_template(t.name, t.source, 23 | root_dir: main.build_dir) 24 | end 25 | end 26 | 27 | def generate_file_from_template(file, template, 28 | root_dir: main.build_dir, 29 | layout: true) 30 | template = Template.new(template, main) 31 | mkpath file.pathmap("%d") 32 | if template.final? 33 | cp template, file 34 | else 35 | expand_template(template, file, root_dir: root_dir, layout: layout) 36 | end 37 | end 38 | 39 | def expand_template(input, output, 40 | root_dir: main.build_dir, 41 | layout: nil, 42 | ** locals, 43 | &block) 44 | layout_file = 45 | case layout 46 | when true then find_layout_for(output) 47 | when String then find_template_for("#{main.build_dir}/#{layout}") 48 | else nil 49 | end 50 | layout_template = layout_file && Template.new(layout_file, main) 51 | if layout_template 52 | say "expand #{input.path} -> #{output} (layout: #{layout_template.path})" 53 | else 54 | say "expand #{input.path} -> #{output}" 55 | end 56 | content = input.render(main, 57 | layout: layout_template, 58 | root_dir: root_dir, 59 | ** locals, 60 | &block) 61 | mkpath output.pathmap("%d") 62 | open(output, "w") do |f| 63 | f.write(content) 64 | end 65 | end 66 | 67 | def find_template_deps_for(path) 68 | return UNMET_DEPENDENCY if dependency_blacklist.include?(path) 69 | find_template_for(path) { [] } 70 | end 71 | 72 | # Search upwards until we find a _layout.* file corresponding to `path`. 73 | def find_layout_for(path) 74 | ext = File.extname(path) 75 | upwards_find_template_for(path.pathmap("%d/_layout#{ext}")) 76 | end 77 | 78 | # Search upwards until we find a template corresponding to target `path`. 79 | def upwards_find_template_for(path) 80 | path = Pathname(path) 81 | base = path.basename 82 | path.dirname.ascend do |dir| 83 | if template = find_template_for((dir + base).to_s){nil} 84 | return template 85 | end 86 | return nil if dir.to_s == main.build_dir 87 | end 88 | nil 89 | end 90 | 91 | # Look up the path of a template corresponding to the given target path. 92 | # Favors user templates over system templates. 93 | # 94 | # @yield if no template is found 95 | # @raise [RuntimeError] if no template is found and no block provided 96 | def find_template_for(path) 97 | logical_path = rel_path(path, main.build_dir) 98 | template = find_user_template_for(logical_path) || 99 | find_system_template_for(logical_path) 100 | template or if block_given? then yield 101 | else raise "No template found for resource #{path}" 102 | end 103 | end 104 | 105 | 106 | def find_user_template_for(path) 107 | FileList["#{user_template_dir}/#{path}*"].first 108 | end 109 | 110 | def find_system_template_for(path) 111 | FileList["#{system_template_dir}/#{path}*"].first 112 | end 113 | 114 | def make_template(path) 115 | Template.new(path, main) 116 | end 117 | 118 | def do_not_generate_deps_for(path) 119 | self.dependency_blacklist << path 120 | end 121 | 122 | def user_template_dir 123 | template_dir 124 | end 125 | 126 | def template_expansion_dir 127 | main.template_build_dir 128 | end 129 | 130 | def template_dir 131 | "templates" 132 | end 133 | 134 | def system_template_dir 135 | File.expand_path("../../../templates", __FILE__) 136 | end 137 | 138 | def dependency_blacklist 139 | @dependency_blacklist ||= [] 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /lib/quarto/uri_helpers.rb: -------------------------------------------------------------------------------- 1 | require "base64" 2 | require "mime/types" 3 | 4 | module Quarto 5 | module UriHelpers 6 | module_function 7 | def data_uri_for_file(file, type=guess_type_of_file(file)) 8 | data = File.read(file) 9 | encoded_data = Base64.strict_encode64(data) 10 | uri = "data:#{type};base64," 11 | uri << encoded_data 12 | uri 13 | end 14 | 15 | def guess_type_of_file(file) 16 | MIME::Types.type_for(file).first 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/quarto/version.rb: -------------------------------------------------------------------------------- 1 | module Quarto 2 | VERSION = "0.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /quarto.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'quarto/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "quarto" 8 | spec.version = Quarto::VERSION 9 | spec.authors = ["Avdi Grimm"] 10 | spec.email = ["avdi@avdi.org"] 11 | spec.description = %q{Yet another ebook publishing toolchain} 12 | spec.summary = %q{Yet another ebook publishing toolchain} 13 | spec.homepage = "" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_runtime_dependency 'rake', '~> 10.0', '~> 10.0' 22 | 23 | spec.add_dependency "nokogiri", "~> 1.6" 24 | spec.add_dependency "fattr", "~> 2.2" 25 | spec.add_dependency "sass", "~> 3.2" 26 | spec.add_dependency "mime-types", "~> 1.24" 27 | spec.add_dependency "doc_raptor", "~> 0.3.2" 28 | spec.add_dependency "dotenv", "~> 0.8.0" 29 | spec.add_dependency "netrc", "~> 0.7.7" 30 | spec.add_dependency "naught", "~>1.0" 31 | spec.add_dependency "tilt", "~> 1.4" 32 | spec.add_dependency "slim", "~> 2.0" 33 | spec.add_dependency "sprockets", "~> 2.12" 34 | 35 | spec.add_development_dependency "bundler", "~> 1.3" 36 | spec.add_development_dependency "rspec", "~> 3.0" 37 | spec.add_development_dependency "rspec-given", "~> 3.1" 38 | spec.add_development_dependency "test_construct", "~> 2.0" 39 | spec.add_development_dependency "yard", "~> 0.8.7" 40 | end 41 | -------------------------------------------------------------------------------- /spec/env.rb: -------------------------------------------------------------------------------- 1 | ORG_VERSION = "8.0.7" 2 | VENDOR_ORG_MODE_DIR = File.expand_path("../../vendor/org/lisp", __FILE__) 3 | -------------------------------------------------------------------------------- /spec/golden/master/rake-codex-with-custom-metadata/build/codex.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World, The Book 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |

Hello, world

22 |

This is the intro

23 |
24 |
25 |
26 |
27 |

Hello again

28 |

This is chapter 1

29 |
30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /spec/golden/master/rake-codex-with-minimal-config/build/codex.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Untitled Book 6 | 7 | 8 | 9 |
10 |
11 |

Hello, world

12 |

This is the intro

13 |
14 |
15 |
16 |
17 |

Hello again

18 |

This is chapter 1

19 |
20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /spec/golden/master/rake-export-with-markdown-sources/build/exports/intro.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 |

Hello, world

15 |

This is the adequate intro

16 | 17 | 18 | -------------------------------------------------------------------------------- /spec/golden/master/rake-export-with-markdown-sources/build/exports/part1/ch1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 |

Hello again

15 |

This is chapter 1

16 | 17 | 18 | -------------------------------------------------------------------------------- /spec/golden/master/rake-export-with-orgmode-sources/build/exports/book.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | book 6 | 7 | 8 | 9 | 10 | 88 | 134 | 135 | 136 |
137 |

book

138 |
139 |

Chapter 1

140 |
141 |

142 | Hello from Org-Mode! 143 |

144 | 145 |
146 | 147 |
puts 1 + 1
148 | 
149 |
150 |
151 |
152 |
153 | 154 | 155 | -------------------------------------------------------------------------------- /spec/golden/master/rake-highlight-highlights-source-listings/build/highlights/3361c5f02e08bd44bde2d42633a2c9be201f7ec4.html: -------------------------------------------------------------------------------- 1 |
puts "hello, world"
2 | 
3 | -------------------------------------------------------------------------------- /spec/golden/master/rake-highlight-highlights-source-listings/build/highlights/b8f5d0e6fa84ab657a95f4e67d1093abcc9dd3df.html: -------------------------------------------------------------------------------- 1 |
int main(int argc, char** argv) {
2 |   printf("Hello, world\n")
3 | }
4 | 
5 | -------------------------------------------------------------------------------- /spec/golden/master/rake-master-builds-a-master-file-and-links-in-images/build/master/images/image1.png: -------------------------------------------------------------------------------- 1 | PRETEND I'M AN IMAGE 2 | -------------------------------------------------------------------------------- /spec/golden/master/rake-master-builds-a-master-file-and-links-in-images/build/master/master.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Untitled Book 6 | 7 | 8 | 9 |
10 |
11 |

12 | Before listing 0 13 |

14 |
puts "hello, world"
15 | 
16 |

17 | After listing 0 18 |

19 |

20 | 21 |

22 |
23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /spec/golden/master/rake-pandoc-epub-epub-generates-epub/build/deliverables/untitled-book.epub.golden_child_unzip/META-INF/container.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /spec/golden/master/rake-pandoc-epub-epub-generates-epub/build/deliverables/untitled-book.epub.golden_child_unzip/content.opf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | urn:uuid:e2848ede-cf97-405b-8b96-63a32cb3266a 5 | Untitled Book 6 | Untitled Book 7 | 2014-06-24T19:22:31Z 8 | en 9 | Avdi Grimm 10 | Copyright © 2014 Avdi Grimm 11 | 2014-06-24T19:22:31Z 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /spec/golden/master/rake-pandoc-epub-epub-generates-epub/build/deliverables/untitled-book.epub.golden_child_unzip/mimetype: -------------------------------------------------------------------------------- 1 | application/epub+zip -------------------------------------------------------------------------------- /spec/golden/master/rake-sections-with-orgmode-sources/build/sections/book.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chapter 1 6 | 7 | 8 |
9 |
10 |

Chapter 1

11 |
12 |

13 | Hello from Org-Mode! 14 |

15 |
16 |             puts 1 + 1
17 | 
18 |           
19 |
20 |
21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /spec/golden/master/rake-signatures-with-markdown-sources/build/signatures/empty.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Untitled Signature 6 | 7 | 8 |
9 |
10 | 11 |
12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/golden/master/rake-signatures-with-markdown-sources/build/signatures/intro.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello, world 6 | 7 | 8 |
9 |
10 |

Hello, world

11 |

This is the adequate intro

12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /spec/golden/master/rake-signatures-with-markdown-sources/build/signatures/part1/ch1.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello again 6 | 7 | 8 |
9 |
10 |

Hello again

11 |

This is chapter 1

12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /spec/golden/master/rake-signatures-with-orgmode-sources/build/signatures/book.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | book 6 | 7 | 8 |
9 |
10 |
11 |

Chapter 1

12 |
13 |

14 | Hello from Org-Mode! 15 |

16 |
17 |               puts 1 + 1
18 | 
19 |             
20 |
21 |
22 |
23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /spec/golden/master/rake-site-build-builds-a-website/build/site/fascicles/001-ch1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Untitled Book 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |

Chapter 1: An unexpected pancake

20 |

In which our hero receives a surprise at breakfast time.

21 |
22 | 23 |
24 | 25 | -------------------------------------------------------------------------------- /spec/golden/master/rake-site-build-builds-a-website/build/site/fascicles/002-ch2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Untitled Book 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |

Chapter 2: Socks and Violets

20 |

In which our hero goes unshod through the flower bed.

21 |
22 | 23 |
24 | 25 | -------------------------------------------------------------------------------- /spec/golden/master/rake-site-build-builds-a-website/build/site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Untitled Book 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

18 | Untitled Book 19 |

20 |

21 | Table of Contents 22 |

23 | 32 | 33 | -------------------------------------------------------------------------------- /spec/golden/master/rake-skeleton/build/listings/3361c5f02e08bd44bde2d42633a2c9be201f7ec4.rb: -------------------------------------------------------------------------------- 1 | puts "hello, world" -------------------------------------------------------------------------------- /spec/golden/master/rake-skeleton/build/listings/b8f5d0e6fa84ab657a95f4e67d1093abcc9dd3df.c: -------------------------------------------------------------------------------- 1 | int main(int argc, char** argv) { 2 | printf("Hello, world\n") 3 | } -------------------------------------------------------------------------------- /spec/golden/master/rake-skeleton/build/skeleton.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Untitled Book 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 | 28 | 29 |

[Missing code listing: build/highlights/3361c5f02e08bd44bde2d42633a2c9be201f7ec4.html]

30 |
31 |
32 |
33 |
34 |
35 |
36 | 37 | 38 |

[Missing code listing: build/highlights/b8f5d0e6fa84ab657a95f4e67d1093abcc9dd3df.html]

39 |
40 |
41 |
42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /spec/golden/master/rake-structure-generates-a-coherent-book-structure-from-heterogenous-inputs/build/master/master.xhtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Untitled Book 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |

The Foreword

28 |

This is the foreword.

29 |
30 |
31 |
32 |
33 |

The Preface

34 |

This is the preface.

35 |
36 |
37 |
38 |
39 |

Chapter 1

40 |

This is chapter 1, written in Markdown.

41 |

Section 1.1

42 |

Subsection 1.1.1

43 |

Subsubsection 1.1.1.1

44 |
Subsubsubsection 1.1.1.1.1
45 |

Section 1.2

46 |

Subsection 1.2.1

47 |

Subsubsection 1.2.1.1

48 |
Subsubsubsection 1.2.1.1.1
49 |
50 |
51 |
52 |
53 |

Chapter 2

54 | 62 |
63 |

Section 2.1

64 |
65 |
66 |
67 |

Subsection 2.1.1

68 |
69 |
70 |
71 |

Subsubsection 2.1.1.1

72 |
73 |
74 |
75 |
Subsubsubscetion 2.1.1.1.1
76 |
77 |
78 |
79 |
80 |
81 |

Section 2.2

82 |
83 |
84 |
85 |

Subsection 2.2.1

86 |
87 |
88 |
89 |

Subsubsection 2.2.1.1

90 |
91 |
92 |
93 |
Subsubsubscetion 2.2.1.1.1
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |

First heading in chapter 3

103 |

This chapter is not contained in an explicit SECTION tag.

104 |
105 |
106 |
107 |
108 |
109 |

First heading of chapter 4

110 |
111 |

112 | This chapter is not explicitly marked as a chapter with an org-mode property. 113 |

114 |
115 |
116 |
117 |
118 |
119 |
120 |

Chapter 5

121 |

This chapter is in a subdirectory.

122 |
123 |
124 | 125 | 126 | -------------------------------------------------------------------------------- /spec/golden/master/rake-structure-generates-a-coherent-book-structure-from-heterogenous-inputs/build/structure.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | title: Untitled Book 3 | authors: 4 | - Avdi Grimm 5 | author: Avdi Grimm 6 | description: '' 7 | date: '2014-08-10T16:01:19-04:00' 8 | types: 9 | - book 10 | master_file: build/master/master.xhtml 11 | codex_file: build/codex.xhtml 12 | spine_file: build/spine.xhtml 13 | skeleton_file: build/skeleton.xhtml 14 | base_dir: "/home/avdi/dev/quarto/spec/golden/actual/rake-structure-generates-a-coherent-book-structure-from-heterogenous-inputs" 15 | children: 16 | - types: 17 | - signature 18 | id: signature-1 19 | signature_export: build/exports/foreward.html 20 | signature_source: foreward.markdown 21 | signature_file: build/signatures/foreward.xhtml 22 | signature_name: foreward 23 | signature_title: The Foreword 24 | name: foreward 25 | title: The Foreword 26 | signature_number: 1 27 | number: 1 28 | fascicle: build/fascicles/001-foreward.xhtml 29 | numbered_name: 001-foreward 30 | children: 31 | - types: 32 | - toplevel 33 | - frontmatter 34 | - foreword 35 | id: toplevel-1 36 | title: The Foreword 37 | name: the-foreword 38 | number: 1 39 | fascicle: build/fascicles/001-foreward.xhtml 40 | toplevel_number: 1 41 | - types: 42 | - signature 43 | id: signature-2 44 | signature_export: build/exports/preface.html 45 | signature_source: preface.markdown 46 | signature_file: build/signatures/preface.xhtml 47 | signature_name: preface 48 | signature_title: The Preface 49 | name: preface 50 | title: The Preface 51 | signature_number: 2 52 | number: 2 53 | fascicle: build/fascicles/002-preface.xhtml 54 | numbered_name: 002-preface 55 | children: 56 | - types: 57 | - toplevel 58 | - frontmatter 59 | - preface 60 | id: toplevel-2 61 | title: The Preface 62 | name: the-preface 63 | number: 1 64 | fascicle: build/fascicles/002-preface.xhtml 65 | toplevel_number: 2 66 | - types: 67 | - signature 68 | id: signature-3 69 | signature_export: build/exports/ch1.html 70 | signature_source: ch1.markdown 71 | signature_file: build/signatures/ch1.xhtml 72 | signature_name: ch1 73 | signature_title: Chapter 1 74 | name: ch1 75 | title: Chapter 1 76 | signature_number: 3 77 | number: 3 78 | fascicle: build/fascicles/003-ch1.xhtml 79 | numbered_name: 003-ch1 80 | children: 81 | - types: 82 | - toplevel 83 | - mainmatter 84 | - chapter 85 | id: toplevel-3 86 | title: Chapter 1 87 | name: chapter-1 88 | number: 1 89 | fascicle: build/fascicles/003-ch1.xhtml 90 | toplevel_number: 3 91 | chapter_number: 1 92 | - types: 93 | - signature 94 | id: signature-4 95 | signature_export: build/exports/ch2.html 96 | signature_source: ch2.org 97 | signature_file: build/signatures/ch2.xhtml 98 | signature_name: ch2 99 | signature_title: This the title of ch2.org 100 | name: ch2 101 | title: This the title of ch2.org 102 | signature_number: 4 103 | number: 4 104 | fascicle: build/fascicles/004-ch2.xhtml 105 | numbered_name: 004-ch2 106 | children: 107 | - types: 108 | - toplevel 109 | - mainmatter 110 | - chapter 111 | id: toplevel-4 112 | title: Chapter 2 113 | name: chapter-2 114 | number: 1 115 | fascicle: build/fascicles/004-ch2.xhtml 116 | toplevel_number: 4 117 | chapter_number: 2 118 | - types: 119 | - signature 120 | id: signature-5 121 | signature_export: build/exports/ch3-implicit.html 122 | signature_source: ch3-implicit.markdown 123 | signature_file: build/signatures/ch3-implicit.xhtml 124 | signature_name: ch3-implicit 125 | signature_title: Title of Chapter 3 126 | name: ch3-implicit 127 | title: Title of Chapter 3 128 | signature_number: 5 129 | number: 5 130 | fascicle: build/fascicles/005-ch3-implicit.xhtml 131 | numbered_name: 005-ch3-implicit 132 | children: 133 | - types: 134 | - toplevel 135 | - mainmatter 136 | - chapter 137 | id: toplevel-5 138 | title: Title of Chapter 3 139 | name: title-of-chapter-3 140 | number: 1 141 | fascicle: build/fascicles/005-ch3-implicit.xhtml 142 | toplevel_number: 5 143 | chapter_number: 3 144 | - types: 145 | - signature 146 | id: signature-6 147 | signature_export: build/exports/ch4-implicit.html 148 | signature_source: ch4-implicit.org 149 | signature_file: build/signatures/ch4-implicit.xhtml 150 | signature_name: ch4-implicit 151 | signature_title: Title of Chapter 4 152 | name: ch4-implicit 153 | title: Title of Chapter 4 154 | signature_number: 6 155 | number: 6 156 | fascicle: build/fascicles/006-ch4-implicit.xhtml 157 | numbered_name: 006-ch4-implicit 158 | children: 159 | - types: 160 | - toplevel 161 | - mainmatter 162 | - chapter 163 | id: toplevel-6 164 | title: Title of Chapter 4 165 | name: title-of-chapter-4 166 | number: 1 167 | fascicle: build/fascicles/006-ch4-implicit.xhtml 168 | toplevel_number: 6 169 | chapter_number: 4 170 | - types: 171 | - signature 172 | id: signature-7 173 | signature_export: build/exports/subdir/ch5.html 174 | signature_source: subdir/ch5.markdown 175 | signature_file: build/signatures/subdir/ch5.xhtml 176 | signature_name: ch5 177 | signature_title: Chapter 5 178 | name: ch5 179 | title: Chapter 5 180 | signature_number: 7 181 | number: 7 182 | fascicle: build/fascicles/007-ch5.xhtml 183 | numbered_name: 007-ch5 184 | children: 185 | - types: 186 | - toplevel 187 | - mainmatter 188 | - chapter 189 | id: toplevel-7 190 | title: Chapter 5 191 | name: chapter-5 192 | number: 1 193 | fascicle: build/fascicles/007-ch5.xhtml 194 | toplevel_number: 7 195 | chapter_number: 5 196 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__)) 2 | require_relative "env" 3 | require "fileutils" 4 | require "pathname" 5 | require "open3" 6 | require "golden_child/rspec" 7 | 8 | begin 9 | # use `bundle install --standalone' to get this... 10 | require_relative '../bundle/bundler/setup' 11 | rescue LoadError 12 | # fall back to regular bundler if the developer hasn't bundled standalone 13 | require 'bundler' 14 | Bundler.setup 15 | end 16 | 17 | require "rspec/given" 18 | require "test_construct/rspec_integration" 19 | require "nokogiri" 20 | 21 | module SpecHelpers 22 | def within_xml(xml_file) 23 | doc = open(xml_file) do |f| 24 | Nokogiri::XML(f) 25 | end 26 | yield doc 27 | end 28 | end 29 | 30 | GoldenChild.configure do |config| 31 | config.env["VENDOR_ORG_MODE_DIR"] = VENDOR_ORG_MODE_DIR 32 | config.add_content_filter("*.xhtml", "**/content.opf") do |file_content| 33 | timestamp_pattern = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}((-\d{2}:\d{2})|Z)/ 34 | file_content.gsub(timestamp_pattern, "1970-01-01-T00:00:00Z") 35 | end 36 | config.add_content_filter("**/content.opf") do |file_content| 37 | urn_pattern = /urn:uuid:[[:alnum:]-]+/ 38 | file_content.gsub(urn_pattern, "urn:uuid:FAKE-FAKE-FAKE") 39 | end 40 | # 2014-06-29 Sun 23:30 41 | config.add_content_filter("*.xhtml") do |file_content| 42 | file_content.gsub(/\d{4}-\d{2}-\d{2} \w{3} \d{2}:\d{2}/, 43 | "1970-01-01 Tue 00:00") 44 | end 45 | config.add_content_filter("**/*") do |file_content| 46 | file_content.gsub(File.expand_path("../..", __FILE__), "/DEVDIR") 47 | end 48 | end 49 | 50 | RSpec.configure do |config| 51 | config.include SpecHelpers 52 | config.expose_current_running_example_as :example 53 | 54 | config.before :each do |example| 55 | @construct = example.metadata[:construct] 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/tasks/codex_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'open3' 3 | 4 | describe "rake codex", golden: true do 5 | specify "with minimal config" do 6 | populate_from("examples/minimal") 7 | 8 | run "rake codex" 9 | 10 | expect("build/codex.xhtml").to match_master 11 | end 12 | 13 | specify "with custom metadata" do 14 | populate_from("examples/metadata") 15 | 16 | run "rake codex" 17 | 18 | expect("build/codex.xhtml").to match_master 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/tasks/export_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'open3' 3 | 4 | 5 | describe "rake export", golden: true do 6 | specify "with markdown sources" do 7 | populate_from "examples/markdown-basic" 8 | 9 | run "rake export" 10 | 11 | expect(%W[build/exports/intro.html 12 | build/exports/part1/ch1.html]).to match_master 13 | end 14 | 15 | specify "with orgmode sources" do 16 | populate_from "examples/orgmode-basic" 17 | run "rake export" 18 | 19 | expect("build/exports/book.html").to match_master 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/tasks/highlight_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "open3" 3 | 4 | describe "rake highlight", golden: true do 5 | it "highlights source listings" do 6 | populate_from("examples/source-listings") 7 | 8 | run "rake highlight" 9 | 10 | expect("build/highlights/3361c5f02e08bd44bde2d42633a2c9be201f7ec4.html"). 11 | to match_master 12 | expect("build/highlights/b8f5d0e6fa84ab657a95f4e67d1093abcc9dd3df.html"). 13 | to match_master 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/tasks/master_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "open3" 3 | 4 | describe "rake master", golden: true do 5 | specify "builds a master file and links in images" do 6 | populate_from("examples/images") 7 | 8 | run "rake master" 9 | 10 | expect("build/master/master.xhtml").to match_master 11 | expect("build/master/images/image1.png").to match_master 12 | end 13 | end 14 | 15 | -------------------------------------------------------------------------------- /spec/tasks/pandoc_epub_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "rake pandoc_epub:epub", golden: true do 4 | it "generates epub" do 5 | populate_from("examples/pandoc_epub") 6 | 7 | run "rake pandoc_epub:epub" 8 | 9 | within_zip("build/deliverables/untitled-book.epub") do |dir| 10 | expect("mimetype").to match_master 11 | expect("content.opf").to match_master 12 | expect("META-INF/container.xml").to match_master 13 | within_xml(dir + "META-INF" + "container.xml") do |doc| 14 | first_rootfile = doc.at_xpath( 15 | "/ocf:container/ocf:rootfiles/ocf:rootfile", 16 | "ocf" => ocf_ns) 17 | expect(first_rootfile["full-path"]).to eq("content.opf") 18 | expect(first_rootfile["media-type"]) 19 | .to eq("application/oebps-package+xml") 20 | end 21 | end 22 | end 23 | let(:ocf_ns) { 24 | "urn:oasis:names:tc:opendocument:xmlns:container" 25 | } 26 | end 27 | -------------------------------------------------------------------------------- /spec/tasks/signatures_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'open3' 3 | 4 | describe "rake signatures", golden: true do 5 | specify "with markdown sources" do 6 | populate_from("examples/markdown-basic") 7 | 8 | run "rake signatures" 9 | 10 | expect("build/signatures/intro.xhtml").to match_master 11 | expect("build/signatures/part1/ch1.xhtml").to match_master 12 | expect("build/signatures/empty.xhtml").to match_master 13 | end 14 | 15 | specify "with orgmode sources" do 16 | populate_from("examples/orgmode-basic") 17 | 18 | run "rake signatures" 19 | 20 | expect("build/signatures/book.xhtml").to match_master 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/tasks/site_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "rake site:build", golden: true do 4 | it "builds a website" do 5 | populate_from("examples/website") 6 | 7 | run "rake site:build" 8 | 9 | expect("build/site/index.html").to match_master 10 | expect("build/site/fascicles/001-ch1.html").to match_master 11 | expect("build/site/fascicles/002-ch2.html").to match_master 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/tasks/skeleton_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "rake skeleton", golden: true do 4 | Given { populate_from("examples/source-listings") } 5 | When { run "rake skeleton" } 6 | Then { expect("build/skeleton.xhtml").to match_master } 7 | And { 8 | expect("build/listings/3361c5f02e08bd44bde2d42633a2c9be201f7ec4.rb"). 9 | to match_master 10 | } 11 | And { 12 | expect("build/listings/b8f5d0e6fa84ab657a95f4e67d1093abcc9dd3df.c"). 13 | to match_master 14 | } 15 | end 16 | -------------------------------------------------------------------------------- /spec/tasks/structure_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "rake structure", golden: true do 4 | it "generates a coherent book structure from heterogenous inputs" do 5 | populate_from("examples/structure") 6 | 7 | run "rake structure" 8 | 9 | expect("build/master/master.xhtml").to match_master 10 | expect("build/structure.yaml").to match_master 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /templates/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } -------------------------------------------------------------------------------- /templates/bower.json.erb: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%= build.name %>", 3 | "private": true 4 | } -------------------------------------------------------------------------------- /templates/site/_book_metadata.html.slim: -------------------------------------------------------------------------------- 1 | link rel="schema.DC" href="http://purl.org/dc/elements/1.1/" 2 | meta name="author" content=build.authors.join(',') 3 | meta name="date" content=build.date 4 | meta name="subject" content=build.description 5 | meta name="generator" content="Quarto #{Quarto::VERSION}" 6 | meta name="DC.title" content=build.title 7 | meta name="DC.creator" content=build.authors.join(',') 8 | meta name="DC.description" content=build.description 9 | meta name="DC.date" content=build.date 10 | meta name="DC.language" content=build.language 11 | meta name="DC.rights" content=build.rights -------------------------------------------------------------------------------- /templates/site/_fascicle.html.slim: -------------------------------------------------------------------------------- 1 | .fascicle 2 | == yield -------------------------------------------------------------------------------- /templates/site/_layout.html.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | html 4 | head 5 | title = build.title 6 | 7 | == render "site/_book_metadata.html" 8 | 9 | body 10 | == yield 11 | -------------------------------------------------------------------------------- /templates/site/_toc.html.slim: -------------------------------------------------------------------------------- 1 | - build.fascicles.each do |fascicle| 2 | li: a href=build.site.fascicle_url(fascicle) = fascicle.title -------------------------------------------------------------------------------- /templates/site/index.html.slim: -------------------------------------------------------------------------------- 1 | h1 = build.title 2 | h3 Table of Contents 3 | nav.toc 4 | ul 5 | == render "site/_toc.html" 6 | -------------------------------------------------------------------------------- /templates/stylesheets/base.scss: -------------------------------------------------------------------------------- 1 | /* Sample style sheet for boom!, the book microformat */ 2 | /* written by Hakon Wium Lie and Bert Bos, November 2005 */ 3 | /* Converted to SCSS and improved by Noel Rappin */ 4 | /* You may reuse this style sheet for any purpose without any fees */ 5 | 6 | html { 7 | margin: 0; 8 | } 9 | 10 | html, p { 11 | font-family: $font, serif; 12 | line-height: 1.4; 13 | } 14 | 15 | body { 16 | margin: 0 0 0 0; 17 | } 18 | 19 | img { 20 | width: 100%; 21 | } 22 | 23 | h1, h2 { 24 | margin: 2em 0 0.5em 0; 25 | } 26 | 27 | h1, h2, h3, h4, h5, h6 { 28 | font-family: $heading_font; 29 | font-weight: bold; 30 | page-break-after: avoid; 31 | color: $heading_color; 32 | } 33 | 34 | h1 { 35 | padding: 0.5em 0 0.5em 0; 36 | margin: 0; 37 | margin-top: 10ex; 38 | font-weight: 900; 39 | text-align: right; 40 | border-bottom: 1pt solid lighten($heading_color, 40%); 41 | } 42 | 43 | h2 { 44 | text-align: left; 45 | font-weight: bold; 46 | } 47 | 48 | h3 { 49 | font-weight: normal; 50 | font-style: normal; 51 | margin-bottom: 0; 52 | padding-bottom: 0; 53 | color: lighten($heading_color, 40%); 54 | } 55 | 56 | h3 + div p:first-of-type, .first-para-after-h3 { 57 | margin-top: 0; 58 | padding-top: 0; 59 | } 60 | 61 | .sidebar-title { 62 | font-family: $heading_font; 63 | font-weight: bold; 64 | text-align: center; 65 | color: $heading_color; 66 | margin: 0.5em 0 0em 0; 67 | font-size: 1.5em; 68 | font-weight: bold; 69 | } 70 | 71 | .issue { 72 | font-size: 1em; 73 | font-weight: bold; 74 | padding: 1em 0px 1em 10px; 75 | margin: 0 200px 0 100px; 76 | background-color: #50b9f9; 77 | } 78 | 79 | q::before { 80 | content: "\201C"; 81 | } 82 | 83 | q::after { 84 | content: "\201D"; 85 | } 86 | 87 | /*p { margin: 0 }*/ 88 | /*p + p { text-indent: 1.3em }*/ 89 | p.sidenote + p, p.caption, p.art { text-indent: 0 } 90 | 91 | p.author { 92 | margin-top: 2em; 93 | text-indent: 0; 94 | text-align: right; 95 | } 96 | 97 | pre { 98 | margin: 1em 1.3em; 99 | border-radius: 1ex; 100 | padding: 1ex; 101 | } 102 | 103 | a { text-decoration: none; } 104 | 105 | /* cross-references */ 106 | 107 | a.pageref::after { content: " on page " target-counter(attr(href), page); } 108 | a.chapref::before { content: " Chapter " target-counter(attr(href), chapter) ", "; } 109 | a.figref { content: " Figure " target-counter(attr(href), figure); } 110 | a.tableref { content: " Table " target-counter(attr(href), figure); } 111 | 112 | figure { 113 | border: solid black 1px; 114 | margin: 5px; 115 | padding: 5px; 116 | figcaption { 117 | font-family: "PT Sans", sans-serif; 118 | font-weight: bold; 119 | display: block; 120 | color: $heading_color; 121 | &::before { 122 | content: " Figure " counter(figure) ": "; 123 | } 124 | 125 | } 126 | } 127 | 128 | /* sidenotes */ 129 | 130 | .sidenote { 131 | float: left; 132 | clear: left; 133 | margin: 0 0 1em -41%; 134 | width: 37%; 135 | font-size: 0.9em; 136 | font-style: normal; 137 | text-indent: 0; 138 | text-align: right; 139 | page-break-inside: avoid; 140 | background: #50b9f9; 141 | } 142 | 143 | .code-filename { 144 | color: $heading_color; 145 | font-family: "PT Sans", sans-serif; 146 | font-weight: bold; 147 | 148 | &:before { 149 | content: "Filename: " 150 | } 151 | } 152 | 153 | .code-caption { 154 | font-family: "PT Sans", sans-serif; 155 | font-weight: bold; 156 | color: $heading_color; 157 | text-align: center; 158 | } 159 | 160 | /* sidebars */ 161 | 162 | .sidebar { 163 | float: right; 164 | clear: right; 165 | /* margin: 0 3em 1em -20%; */ 166 | margin: 0 1.5em 1em 1em; 167 | width: 40%; 168 | border-left: thin solid black; 169 | text-align: justify; 170 | /*background: #DDFFFF;*/ 171 | padding: 0.5em 0.5em; 172 | // page-break-inside: avoid; 173 | /*column-count: 2; 174 | column-gap: 1.5em; */ 175 | 176 | h3 { 177 | margin-top: 0; 178 | text-align: center; 179 | border-bottom: thin solid black; 180 | } 181 | 182 | pre { 183 | } 184 | 185 | .sidebar-body { 186 | margin-bottom: 1em; 187 | } 188 | } 189 | 190 | /* figures and tables*/ 191 | 192 | div.figure { 193 | margin: 1em 0; 194 | counter-increment: figure; 195 | } 196 | 197 | figure { 198 | margin: 1em 0; 199 | counter-increment: figure; 200 | text-align: center; 201 | } 202 | 203 | div.CodeRay { 204 | counter-increment: sample; 205 | } 206 | 207 | div.code-caption::before { 208 | content: "Sample " counter(chapter) "-" counter(section) "-" counter(sample) ": "; 209 | } 210 | 211 | div.figure .caption, div.table .caption { 212 | float: left; 213 | clear: left; 214 | width: 37%; 215 | text-align: right; 216 | font-size: 0.9em; 217 | margin: 0 0 1.2em -40%; 218 | } 219 | 220 | ol li, ul li { 221 | margin-bottom: 1em; 222 | } 223 | 224 | 225 | 226 | div.figure .caption::before { 227 | content: "Figure " counter(figure) ": "; 228 | font-weight: bold; 229 | } 230 | 231 | div.table .caption::before { 232 | content: "Table " counter(table) ": "; 233 | font-weight: bold; 234 | } 235 | 236 | div.table { 237 | margin: 1em 0; 238 | counter-increment: table; 239 | } 240 | 241 | div.table th { 242 | text-align: left; 243 | } 244 | 245 | table th, table td { 246 | text-align: left; 247 | padding-right: 1em; 248 | border-top: none; 249 | border-bottom: thin dotted; 250 | } 251 | 252 | table.lined td, table.lined th { 253 | border-top: none; 254 | border-bottom: thin dotted; 255 | padding-top: 0.2em; 256 | padding-bottom: 0.2em; 257 | } 258 | 259 | 260 | /* footnotes */ 261 | 262 | .footnote { 263 | display: none; /* default rule */ 264 | display: prince-footnote; /* prince-specific rules */ 265 | position: footnote; 266 | footnote-style-position: inside; 267 | counter-increment: footnote; 268 | // margin-left: 1.4em; 269 | line-height: 1.4; 270 | } 271 | 272 | a.footnote { 273 | display: inline; 274 | margin-left: 0em; 275 | line-height: 1; 276 | } 277 | 278 | .footnote code, .footnote pre { 279 | } 280 | 281 | .footnote::footnote-call { 282 | vertical-align: super; 283 | font-size: 80%; 284 | } 285 | 286 | .footnote::footnote-marker { 287 | vertical-align: super; 288 | color: green; 289 | padding-right: 0.4em; 290 | } 291 | 292 | .frontcover img { 293 | left: 0; top: 0; 294 | z-index: -1; 295 | } 296 | 297 | .frontcover h1 { 298 | color: white; 299 | font-weight: normal; 300 | } 301 | 302 | .frontcover h2 { 303 | color: black; 304 | background: white; 305 | font-weight: normal; 306 | padding: 0.2em 5em 0.2em 1em; 307 | letter-spacing: 0.15em; 308 | } 309 | 310 | .frontcover h3 { 311 | color: white; 312 | font-weight: normal; 313 | } 314 | 315 | .frontcover p { 316 | color: black; 317 | font-weight: bold; 318 | text-transform: uppercase; 319 | } 320 | 321 | 322 | /* titlepage, halftitlepage */ 323 | 324 | .titlepage h1, .halftitlepage h1 { margin-bottom: 2em; } 325 | .titlepage h2, .halftitlepage h2 { 326 | font-size: 1.2em; 327 | margin-bottom: 3em; 328 | } 329 | .titlepage h3, .halftitlepage h3 { font-size: 1em; margin-bottom: 3em; } 330 | .titlepage p, .halftitlepage p { 331 | font-size: 1.4em; 332 | font-weight: bold; 333 | margin: 0; 334 | padding: 0; 335 | } 336 | 337 | section.chapter:first-of-type { 338 | counter-reset: page 1; 339 | } 340 | 341 | /* chapter numbers */ 342 | 343 | section.chapter { 344 | counter-increment: chapter; 345 | counter-reset: section; 346 | } 347 | 348 | h1::before { 349 | white-space: pre; 350 | font-size: 50%; 351 | text-align: right; 352 | } 353 | 354 | section.chapter h1::before { 355 | content: "Chapter " counter(chapter) " \A"; 356 | } 357 | 358 | .frontcover h1::before, .titlepage h1::before, .halftitlepage h1::before { 359 | content: normal; /* that is, none */ 360 | } 361 | 362 | h1 { string-set: header content();} 363 | section.chapter h1 { string-set: header "Chapter " counter(chapter) ": " content(); } 364 | 365 | section.chapter h2 { counter-increment: section; counter-reset: sample; } 366 | 367 | section.chapter h2::before { 368 | color: lighten($heading_color, 40%); 369 | content: counter(chapter) "." counter(section) " "; 370 | } 371 | 372 | //h2 { string-set: header content();} 373 | section.chapter h2 { 374 | string-set: header "Section " counter(chapter) "." counter(section) ": " content(); 375 | } 376 | 377 | .frontcover h1::before, .titlepage h1::before, .halftitlepage h1::before { 378 | content: normal; /* that is, none */ 379 | } 380 | 381 | 382 | /* index */ 383 | 384 | ul.index { 385 | list-style-type: none; 386 | margin: 0; padding: 0; 387 | column-count: 2; 388 | column-gap: 1em; 389 | } 390 | 391 | ul.index a::after { content: ", " target-counter(attr(href), page); } 392 | 393 | 394 | span.element, span.attribute { 395 | text-transform: uppercase; 396 | font-weight: bold; 397 | font-size: 80%; 398 | } 399 | span.property { font-weight: bold } 400 | pre, code, span.css, span.value, span.declaration { 401 | color: $heading_color; 402 | font-family: "Source Code Pro", "Inconsolata", "Anonymous Pro", 403 | "DejaVu Sans Mono", "Lucida Console", "Lucida Sans Typewriter", monospace; 404 | } 405 | 406 | pre { 407 | font-size: 80%; 408 | page-break-inside: avoid; 409 | } 410 | 411 | .imprint { 412 | .title { 413 | font-size: 2.5em; 414 | font-weight: 900; 415 | margin-left: 25px; 416 | margin-right: 25px; 417 | text-align: center; 418 | } 419 | 420 | .subtitle { 421 | font-size: 2em; 422 | font-weight: bold; 423 | margin-left: 25px; 424 | margin-right: 25px; 425 | text-align: center; 426 | } 427 | 428 | p { 429 | text-align: center; 430 | font-size: 75%; 431 | } 432 | } 433 | 434 | .dedication p { 435 | text-align: center; 436 | font-style: italic; 437 | } 438 | 439 | @media screen, handheld { 440 | .frontcover, .halftitlepage, .titlepage, .imprint, 441 | .dedication, .foreword, .toc, .index { display: none } 442 | } 443 | 444 | 445 | @media print, handheld { 446 | pre { 447 | white-space: pre-wrap; 448 | } 449 | } 450 | 451 | @media screen { 452 | pre { 453 | overflow: auto; 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /templates/stylesheets/code.scss: -------------------------------------------------------------------------------- 1 | .hll { background-color: #ffffcc } 2 | .c { color: #888888 } /* Comment */ 3 | .err { color: #FF0000; background-color: #FFAAAA } /* Error */ 4 | .k { color: #008800; font-weight: bold } /* Keyword */ 5 | .o { color: #333333 } /* Operator */ 6 | .cm { color: #888888 } /* Comment.Multiline */ 7 | .cp { color: #557799 } /* Comment.Preproc */ 8 | .c1 { color: #888888 } /* Comment.Single */ 9 | .cs { color: #cc0000; font-weight: bold } /* Comment.Special */ 10 | .gd { color: #A00000 } /* Generic.Deleted */ 11 | .ge { font-style: italic } /* Generic.Emph */ 12 | .gr { color: #FF0000 } /* Generic.Error */ 13 | .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 14 | .gi { color: #00A000 } /* Generic.Inserted */ 15 | .go { color: #888888 } /* Generic.Output */ 16 | .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 17 | .gs { font-weight: bold } /* Generic.Strong */ 18 | .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 19 | .gt { color: #0044DD } /* Generic.Traceback */ 20 | .kc { color: #008800; font-weight: bold } /* Keyword.Constant */ 21 | .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */ 22 | .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */ 23 | .kp { color: #003388; font-weight: bold } /* Keyword.Pseudo */ 24 | .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */ 25 | .kt { color: #333399; font-weight: bold } /* Keyword.Type */ 26 | .m { color: #6600EE; font-weight: bold } /* Literal.Number */ 27 | .s { background-color: #fff0f0 } /* Literal.String */ 28 | .na { color: #0000CC } /* Name.Attribute */ 29 | .nb { color: #007020 } /* Name.Builtin */ 30 | .nc { color: #BB0066; font-weight: bold } /* Name.Class */ 31 | .no { color: #003366; font-weight: bold } /* Name.Constant */ 32 | .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 33 | .ni { color: #880000; font-weight: bold } /* Name.Entity */ 34 | .ne { color: #FF0000; font-weight: bold } /* Name.Exception */ 35 | .nf { color: #0066BB; font-weight: bold } /* Name.Function */ 36 | .nl { color: #997700; font-weight: bold } /* Name.Label */ 37 | .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 38 | .nt { color: #007700 } /* Name.Tag */ 39 | .nv { color: #996633 } /* Name.Variable */ 40 | .ow { color: #000000; font-weight: bold } /* Operator.Word */ 41 | .w { color: #bbbbbb } /* Text.Whitespace */ 42 | .mf { color: #6600EE; font-weight: bold } /* Literal.Number.Float */ 43 | .mh { color: #005588; font-weight: bold } /* Literal.Number.Hex */ 44 | .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ 45 | .mo { color: #4400EE; font-weight: bold } /* Literal.Number.Oct */ 46 | .sb { background-color: #fff0f0 } /* Literal.String.Backtick */ 47 | .sc { color: #0044DD } /* Literal.String.Char */ 48 | .sd { color: #DD4422 } /* Literal.String.Doc */ 49 | .s2 { background-color: #fff0f0 } /* Literal.String.Double */ 50 | .se { color: #666666; font-weight: bold; background-color: #fff0f0 } /* Literal.String.Escape */ 51 | .sh { background-color: #fff0f0 } /* Literal.String.Heredoc */ 52 | .si { background-color: #eeeeee } /* Literal.String.Interpol */ 53 | .sx { color: #DD2200; background-color: #fff0f0 } /* Literal.String.Other */ 54 | .sr { color: #000000; background-color: #fff0ff } /* Literal.String.Regex */ 55 | .s1 { background-color: #fff0f0 } /* Literal.String.Single */ 56 | .ss { color: #AA6600 } /* Literal.String.Symbol */ 57 | .bp { color: #007020 } /* Name.Builtin.Pseudo */ 58 | .vc { color: #336699 } /* Name.Variable.Class */ 59 | .vg { color: #dd7700; font-weight: bold } /* Name.Variable.Global */ 60 | .vi { color: #3333BB } /* Name.Variable.Instance */ 61 | .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */ 62 | -------------------------------------------------------------------------------- /templates/stylesheets/epub2.scss: -------------------------------------------------------------------------------- 1 | @import "toc"; 2 | 3 | nav[epub\:type~='toc'] { 4 | @include toc; 5 | } 6 | -------------------------------------------------------------------------------- /templates/stylesheets/epub3.scss: -------------------------------------------------------------------------------- 1 | @import "toc"; 2 | 3 | @namespace epub "http://www.idpf.org/2007/ops"; 4 | 5 | nav[epub|type~='toc'], nav[epub\:type~='toc'], #TOC { 6 | @include toc; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /templates/stylesheets/pages.scss: -------------------------------------------------------------------------------- 1 | // A book consists of different types of sections. We propose to use 2 | // DIV elements with these class names: 3 | 4 | // frontcover 5 | // halftitlepage: contains the title of the book 6 | // titlepage: contains the title of the book, name of author(s) and publisher 7 | // imprint: left page with copyright, publisher, library printing information 8 | // dedication: right page with short dedication 9 | // foreword: written by someone other than the author(s) 10 | // toc: table of contents 11 | // preface: preface, including acknowledgements 12 | // chapter: each chapter is given its own DIV element 13 | // references: contains list of references 14 | // appendix: each appendix is given its own 15 | // bibliography 16 | // glossary 17 | // index 18 | // colophon: describes how the book was produced 19 | // backcover 20 | 21 | // A book will use several of the types listed above, but few books 22 | // will use all of them. 23 | 24 | @page { 25 | margin: 27mm 16mm 27mm 16mm; 26 | size: $print_page_width $print_page_height; 27 | 28 | @footnotes { 29 | border-top: thin solid black; 30 | padding-top: 0.3em; 31 | margin-top: 0.6em; 32 | margin-left: 0%; 33 | margin-bottom: 1em; 34 | } 35 | } 36 | 37 | 38 | /* define default page and names pages: cover, blank, frontmatter */ 39 | 40 | @page :left { 41 | @top-left { 42 | font: 11pt $font, serif; 43 | content: $title; 44 | vertical-align: bottom; 45 | padding-bottom: 0.5em; 46 | border-bottom: thin solid black; 47 | } 48 | 49 | @bottom-left { 50 | font: 11pt $font, serif; 51 | content: counter(page); 52 | padding-top: 0.5em; 53 | vertical-align: top; 54 | border-top: thin solid black; 55 | } 56 | 57 | @bottom-right { 58 | font: 11pt $font, serif; 59 | content: $rslug; 60 | padding-top: 0.5em; 61 | text-align: right; 62 | vertical-align: top; 63 | border-top: thin solid black; 64 | } 65 | } 66 | 67 | @page :right { 68 | @top-right { 69 | font: 11pt $font, serif; 70 | content: string(header, first); 71 | vertical-align: bottom; 72 | padding-bottom: 0.5em; 73 | margin-bottom: 0.5 em; 74 | border-bottom: thin solid black; 75 | } 76 | 77 | @bottom-left { 78 | font: 11pt $font, serif; 79 | content: $lslug; 80 | padding-top: 0.5em; 81 | vertical-align: top; 82 | border-top: thin solid black; 83 | } 84 | 85 | @bottom-right { 86 | font: 11pt $font, serif; 87 | content: counter(page); 88 | text-align: right; 89 | vertical-align: top; 90 | padding-top: 0.5em; 91 | border-top: thin solid black; 92 | } 93 | } 94 | 95 | @page frontmatter :left { 96 | @top-left { 97 | font: 11pt $font, serif; 98 | content: string(title); 99 | vertical-align: bottom; 100 | padding-bottom: 2em; 101 | } 102 | 103 | @bottom-left { 104 | font: 11pt $font, serif; 105 | content: counter(page, lower-roman); 106 | padding-top: 2em; 107 | vertical-align: top; 108 | } 109 | } 110 | 111 | @page cover { margin: 0; } 112 | 113 | @page frontmatter :right { 114 | @top-right { 115 | font: 11pt $font, serif; 116 | content: string(header, first); 117 | vertical-align: bottom; 118 | padding-bottom: 2em; 119 | } 120 | 121 | @bottom-right { 122 | font: 11pt $font, serif; 123 | content: counter(page, lower-roman); 124 | text-align: right; 125 | vertical-align: top; 126 | padding-top: 2em; 127 | } 128 | } 129 | 130 | @page blank :left { 131 | @top-left { content: normal } 132 | @bottom-left { content: normal } 133 | } 134 | 135 | @page blank :right { 136 | @top-right { content: normal } 137 | @bottom-right { content: normal } 138 | } 139 | 140 | .halftitlepage, .titlepage, .imprint, .dedication { page: blank } 141 | .foreword, .toc, #TOC, .preface { page: frontmatter } 142 | 143 | /* page breaks */ 144 | 145 | .frontcover, .halftitlepage, .titlepage { 146 | page-break-before: right ; 147 | } 148 | 149 | .imprint { 150 | page-break-before: always; 151 | } 152 | 153 | .dedication, .foreword, .toc, .preface, section.chapter, .reference, 154 | .appendix, .bibliography, .glossary, .index, .colophon, section.chapter { 155 | page-break-before: always 156 | } 157 | 158 | .backcover { 159 | page-break-before: left; 160 | page: cover; 161 | position: absolute; 162 | width: print_page_width; 163 | height: print_page_height; 164 | left: 0; top: 0; 165 | z-index: -1; 166 | } 167 | 168 | .frontcover { 169 | page: cover; 170 | background-repeat: no-repeat; 171 | background-position: center center; 172 | background-color: $cover_color; 173 | background-size: contain; 174 | position: absolute; 175 | /* background-image-resolution: 275dpi; */ 176 | width: $print_page_width; 177 | height: $print_page_height; 178 | margin: 0px; 179 | padding: 0px; 180 | left: 0; top: 0; 181 | z-index: -1; 182 | 183 | img { 184 | width: 100%; 185 | } 186 | } 187 | 188 | .white_page { page: cover; } 189 | -------------------------------------------------------------------------------- /templates/stylesheets/pdf.scss: -------------------------------------------------------------------------------- 1 | @import "toc"; 2 | 3 | /* Note for the ages: PrinceXML seems not to like finding this syntax: 4 | /* nav[epub|type~='toc'] Even though it's the #TOC selector that 5 | /* matters when PrinceXML is building a PDF, the presence of the CSS3 6 | /* namespace syntax breaks the whole rule. */ 7 | 8 | #TOC { 9 | page: frontmatter; 10 | 11 | @include toc; 12 | } 13 | 14 | @media print { 15 | html, p { 16 | font-size: 12pt; 17 | } 18 | h1 { 19 | page-break-before: right; 20 | } 21 | h2 { 22 | page-break-before: always; 23 | } 24 | h3 { 25 | font-size: 12pt; 26 | } 27 | .sidebar { 28 | font-size: 10pt; 29 | pre { 30 | font-size: 10pt; 31 | } 32 | } 33 | .footnote { 34 | font-size: 9pt; 35 | code, pre { 36 | font-size: 10pt; 37 | } 38 | } 39 | 40 | a.footnote { 41 | font-size: 9pt; 42 | } 43 | div.frontcover img { 44 | position: absolute; 45 | width: $print_page_width; 46 | height: $print_page_height; 47 | } 48 | div.frontcover h1 { 49 | position: absolute; 50 | left: 2cm; top: 1cm; 51 | font-size: 44pt; 52 | } 53 | div.frontcover h2 { 54 | position: absolute; 55 | right: 0; top: 5cm; 56 | font-size: 16pt; 57 | } 58 | div.frontcover h3 { 59 | position: absolute; 60 | left: 2cm; top: 7cm; 61 | font-size: 24pt; 62 | } 63 | div.frontcover p { 64 | position: absolute; 65 | left: 2cm; bottom: 1.5cm; 66 | font-size: 24pt; 67 | } 68 | pre { 69 | font-size: 10pt; 70 | } 71 | 72 | .chapter a[href]::after { 73 | content: " [page " target-counter(attr(href), page) "]" 74 | } 75 | 76 | a { 77 | color: inherit; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /templates/stylesheets/toc.scss: -------------------------------------------------------------------------------- 1 | @mixin toc { 2 | ul, ol { 3 | padding-left: 0; 4 | margin-left: 0; 5 | list-style-type: none; 6 | 7 | ul ul, ol ol { 8 | display: none; 9 | } 10 | 11 | li { 12 | a:after { 13 | font-weight: normal; 14 | content: leader(".") target-counter(attr(href), page); 15 | } 16 | margin: 0; 17 | padding: 0; 18 | } 19 | 20 | & > li { 21 | font-weight: bold; 22 | } 23 | 24 | ul li, ol li { 25 | font-weight: normal; 26 | list-style-type: none; 27 | } 28 | 29 | & > ul, & > ol { 30 | page-break-inside: avoid; 31 | } 32 | 33 | ul, ol { 34 | margin-left: 0.5em; 35 | padding-left: 0.5em; 36 | } 37 | } 38 | } 39 | --------------------------------------------------------------------------------