summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2020-08-31 09:59:04 +0300
committerLars Wirzenius <liw@liw.fi>2020-09-01 09:24:59 +0300
commite265cfdc9f6afef7f969283df7f63148d91b857f (patch)
tree973041ff38944c41f10276f36dd9f197e78d7dc3
parentb384358ac0ddcf6e8baae1c280f4d7d9930754ad (diff)
downloadsubplot-e265cfdc9f6afef7f969283df7f63148d91b857f.tar.gz
feat(docgen): typeset links as footnotes in PDF
When reading a PDF printed on paper or on a reMarkable tablet, it's not possible to see that there even is a link in a PDF. Make this more visible by typesetting the link URL as a footnote. This is not done on HTML output as web pages are read on browsers that make links easy to see. This is the first time the AST is transformed by docgen differently based on the output format. The decision of what should be done is a policy decision, best done at the topmost level: in the main function of docgen. The result of that decidion (turn links into footnotes or not) is communicated from docgen main into the ast.rs module via a new Style struct. This mechanism can later be extended for other typesetting style decisions (e.g., what fonts to use).
-rw-r--r--src/ast.rs41
-rw-r--r--src/bin/sp-codegen.rs5
-rw-r--r--src/bin/sp-docgen.rs8
-rw-r--r--src/bin/sp-extract.rs5
-rw-r--r--src/bin/sp-filter.rs5
-rw-r--r--src/bin/sp-meta.rs5
-rw-r--r--src/lib.rs1
-rw-r--r--src/typeset.rs12
-rw-r--r--src/visitor/typesetting.rs21
9 files changed, 82 insertions, 21 deletions
diff --git a/src/ast.rs b/src/ast.rs
index 41a1461..2ff25ca 100644
--- a/src/ast.rs
+++ b/src/ast.rs
@@ -10,6 +10,7 @@ use crate::ScenarioStep;
use crate::{Result, SubplotError};
use std::collections::HashSet;
+use std::default::Default;
use std::ops::Deref;
use std::path::{Path, PathBuf};
@@ -61,7 +62,8 @@ static KNOWN_PANDOC_CLASSES: &[&str] = &["numberLines"];
///
/// use subplot;
/// let basedir = std::path::Path::new(".");
-/// let doc = subplot::Document::from_file(&basedir, filename).unwrap();
+/// let style = subplot::Style::default();
+/// let doc = subplot::Document::from_file(&basedir, filename, style).unwrap();
/// assert_eq!(doc.files(), &[]);
/// ~~~~
#[derive(Debug)]
@@ -70,20 +72,33 @@ pub struct Document {
ast: Pandoc,
meta: Metadata,
files: DataFiles,
+ style: Style,
}
impl<'a> Document {
- fn new(markdowns: Vec<PathBuf>, ast: Pandoc, meta: Metadata, files: DataFiles) -> Document {
+ fn new(
+ markdowns: Vec<PathBuf>,
+ ast: Pandoc,
+ meta: Metadata,
+ files: DataFiles,
+ style: Style,
+ ) -> Document {
Document {
markdowns,
ast,
meta,
files,
+ style,
}
}
/// Construct a Document from a JSON AST
- pub fn from_json<P>(basedir: P, markdowns: Vec<PathBuf>, json: &str) -> Result<Document>
+ pub fn from_json<P>(
+ basedir: P,
+ markdowns: Vec<PathBuf>,
+ json: &str,
+ style: Style,
+ ) -> Result<Document>
where
P: AsRef<Path>,
{
@@ -96,7 +111,7 @@ impl<'a> Document {
return Err(linter.issues.remove(0));
}
let files = DataFiles::new(&mut ast);
- Ok(Document::new(markdowns, ast, meta, files))
+ Ok(Document::new(markdowns, ast, meta, files, style))
}
/// Construct a Document from a named file.
@@ -104,7 +119,7 @@ impl<'a> Document {
/// The file can be in any format Pandoc understands. This runs
/// Pandoc to parse the file into an AST, so it can be a little
/// slow.
- pub fn from_file(basedir: &Path, filename: &Path) -> Result<Document> {
+ pub fn from_file(basedir: &Path, filename: &Path, style: Style) -> Result<Document> {
let markdowns = vec![filename.to_path_buf()];
let mut pandoc = pandoc::new();
@@ -124,7 +139,7 @@ impl<'a> Document {
pandoc::PandocOutput::ToFile(_) => return Err(SubplotError::NotJson),
pandoc::PandocOutput::ToBuffer(o) => o,
};
- let doc = Document::from_json(basedir, markdowns, &output)?;
+ let doc = Document::from_json(basedir, markdowns, &output, style)?;
Ok(doc)
}
@@ -242,7 +257,8 @@ impl<'a> Document {
/// Typeset a Subplot document.
pub fn typeset(&mut self) {
- let mut visitor = visitor::TypesettingVisitor::new(&self.meta.bindings());
+ let mut visitor =
+ visitor::TypesettingVisitor::new(self.style.clone(), &self.meta.bindings());
visitor.walk_pandoc(&mut self.ast);
}
@@ -426,3 +442,14 @@ mod test_extract {
}
}
}
+
+/// Typesetting style configuration for documents.
+#[derive(Clone, Debug, Default)]
+pub struct Style {
+ /// Should hyperlinks in the document be rendered as footnotes or endnotes?
+ ///
+ /// A link is like the HTML `<a>` element. The choice of footnote
+ /// versus endnote is made by the typesetting backend. HTML uses
+ /// endnotes, PDF uses footnotes.
+ pub links_as_notes: bool,
+}
diff --git a/src/bin/sp-codegen.rs b/src/bin/sp-codegen.rs
index b95cd5c..177f2ac 100644
--- a/src/bin/sp-codegen.rs
+++ b/src/bin/sp-codegen.rs
@@ -4,12 +4,13 @@ use std::process::Command;
use anyhow::Result;
use structopt::StructOpt;
-use subplot::{generate_test_program, template_spec, Document, SubplotError, TemplateSpec};
+use subplot::{generate_test_program, template_spec, Document, Style, SubplotError, TemplateSpec};
fn main() -> Result<()> {
let opt = Opt::from_args();
let basedir = subplot::get_basedir_from(&opt.filename)?;
- let mut doc = Document::from_file(&basedir, &opt.filename)?;
+ let style = Style::default();
+ let mut doc = Document::from_file(&basedir, &opt.filename, style)?;
doc.lint()?;
let spec = template_spec(&opt.templates, &doc)?;
diff --git a/src/bin/sp-docgen.rs b/src/bin/sp-docgen.rs
index 9aa592e..0941392 100644
--- a/src/bin/sp-docgen.rs
+++ b/src/bin/sp-docgen.rs
@@ -1,4 +1,5 @@
use chrono::{Local, TimeZone};
+use std::ffi::OsString;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
@@ -7,7 +8,7 @@ use structopt::StructOpt;
use anyhow::Result;
-use subplot::get_basedir_from;
+use subplot::{get_basedir_from, Document, Style};
// Define the command line arguments.
#[derive(Debug, StructOpt)]
@@ -32,8 +33,11 @@ fn main() -> Result<()> {
let first_file = &opt.filenames[0];
+ let mut style = Style::default();
+ style.links_as_notes = opt.output.extension() == Some(&OsString::from("pdf"));
+
let basedir = get_basedir_from(first_file)?;
- let mut doc = subplot::Document::from_file(&basedir, &first_file)?;
+ let mut doc = Document::from_file(&basedir, &first_file, style)?;
doc.lint()?;
// Metadata date from command line or file mtime. However, we
diff --git a/src/bin/sp-extract.rs b/src/bin/sp-extract.rs
index d63fa0a..2afeadd 100644
--- a/src/bin/sp-extract.rs
+++ b/src/bin/sp-extract.rs
@@ -4,7 +4,7 @@ use std::path::PathBuf;
use structopt::StructOpt;
-use subplot::{DataFile, Document, SubplotError};
+use subplot::{DataFile, Document, Style, SubplotError};
#[derive(Debug, StructOpt)]
#[structopt(name = "sp-meta", about = "Show Subplot document metadata.")]
@@ -25,7 +25,8 @@ struct Opt {
fn main() -> Result<()> {
let opt = Opt::from_args();
let basedir = subplot::get_basedir_from(&opt.filename)?;
- let doc = Document::from_file(&basedir, &opt.filename)?;
+ let style = Style::default();
+ let doc = Document::from_file(&basedir, &opt.filename, style)?;
for filename in opt.embedded {
let file = get_embedded(&doc, &filename)?;
diff --git a/src/bin/sp-filter.rs b/src/bin/sp-filter.rs
index 5b5dfa4..08f7261 100644
--- a/src/bin/sp-filter.rs
+++ b/src/bin/sp-filter.rs
@@ -1,13 +1,14 @@
use anyhow::Result;
use std::io::{self, Read, Write};
-use subplot::Document;
+use subplot::{Document, Style};
fn main() -> Result<()> {
let mut buffer = String::new();
let mut stdin = io::stdin();
stdin.read_to_string(&mut buffer)?;
let basedir = std::path::Path::new(".");
- let mut doc = Document::from_json(&basedir, vec![], &buffer)?;
+ let style = Style::default();
+ let mut doc = Document::from_json(&basedir, vec![], &buffer, style)?;
doc.typeset();
let bytes = doc.ast()?.into_bytes();
io::stdout().write_all(&bytes)?;
diff --git a/src/bin/sp-meta.rs b/src/bin/sp-meta.rs
index b693d26..c633c59 100644
--- a/src/bin/sp-meta.rs
+++ b/src/bin/sp-meta.rs
@@ -6,7 +6,7 @@ use std::str::FromStr;
use serde::Serialize;
use structopt::StructOpt;
-use subplot::Document;
+use subplot::{Document, Style};
#[derive(Debug)]
enum OutputFormat {
@@ -116,7 +116,8 @@ impl Metadata {
fn main() -> Result<()> {
let opt = Opt::from_args();
let basedir = subplot::get_basedir_from(&opt.filename)?;
- let mut doc = Document::from_file(&basedir, &opt.filename)?;
+ let style = Style::default();
+ let mut doc = Document::from_file(&basedir, &opt.filename, style)?;
let meta = Metadata::try_from(&mut doc)?;
match opt.output_format {
diff --git a/src/lib.rs b/src/lib.rs
index 842a264..051ef0b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -30,6 +30,7 @@ pub use metadata::Metadata;
mod ast;
pub use ast::Document;
+pub use ast::Style;
mod scenarios;
pub use scenarios::Scenario;
diff --git a/src/typeset.rs b/src/typeset.rs
index 32956c6..40cbef6 100644
--- a/src/typeset.rs
+++ b/src/typeset.rs
@@ -9,6 +9,7 @@ use crate::{DotMarkup, GraphMarkup, PlantumlMarkup};
use pandoc_ast::Attr;
use pandoc_ast::Block;
use pandoc_ast::Inline;
+use pandoc_ast::Target;
/// Typeset an error as a Pandoc AST Block element.
pub fn error(err: SubplotError) -> Block {
@@ -121,6 +122,17 @@ fn captured(s: &str) -> Inline {
Inline::Strong(vec![inlinestr(s)])
}
+/// Typeset a link as a note.
+pub fn link_as_note(attr: Attr, text: Vec<Inline>, target: Target) -> Inline {
+ let (url, _) = target.clone();
+ let url = Inline::Code(attr.clone(), url);
+ let link = Inline::Link(attr.clone(), vec![url], target);
+ let note = Inline::Note(vec![Block::Para(vec![link])]);
+ let mut text = text;
+ text.push(note);
+ Inline::Span(attr, text)
+}
+
// Take a dot graph, render it as SVG, and return an AST Block
// element. The Block will contain the SVG data. This allows the graph
// to be rendered without referending external entities.
diff --git a/src/visitor/typesetting.rs b/src/visitor/typesetting.rs
index af6ea01..deffcd9 100644
--- a/src/visitor/typesetting.rs
+++ b/src/visitor/typesetting.rs
@@ -1,19 +1,20 @@
use crate::panhelper;
use crate::typeset;
-use crate::Bindings;
+use crate::{Bindings, Style};
-use pandoc_ast::{Block, MutVisitor};
+use pandoc_ast::{Block, Inline, MutVisitor};
/// Visitor for the pandoc AST.
///
/// This includes rendering stuff which we find as we go
pub struct TypesettingVisitor<'a> {
+ style: Style,
bindings: &'a Bindings,
}
impl<'a> TypesettingVisitor<'a> {
- pub fn new(bindings: &'a Bindings) -> Self {
- TypesettingVisitor { bindings }
+ pub fn new(style: Style, bindings: &'a Bindings) -> Self {
+ TypesettingVisitor { style, bindings }
}
}
@@ -46,4 +47,16 @@ impl<'a> MutVisitor for TypesettingVisitor<'a> {
}
}
}
+ fn visit_vec_inline(&mut self, vec_inline: &mut Vec<Inline>) {
+ for inline in vec_inline {
+ match inline {
+ Inline::Link(attr, vec, target) if self.style.links_as_notes => {
+ *inline = typeset::link_as_note(attr.clone(), vec.to_vec(), target.clone());
+ }
+ _ => {
+ self.visit_inline(inline);
+ }
+ }
+ }
+ }
}