├── .gitignore ├── .rspec ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── Makefile ├── README.md ├── Vagrantfile ├── assets ├── default │ ├── template-metadata.tex │ └── template.tex ├── ew │ ├── ew-logo-large.png │ └── header.tex └── sample │ ├── UNlogo.png │ └── header.tex ├── bin └── authorize.rb ├── build ├── .gitkeep └── example │ ├── SHbO80B3zYiLwN09wcE1jxTyLWaBsr7llKXblVz0EwCQsT2EsGznwPa4YwxgN3BwyMSEZhyazyYEoA5TNEBswNe6FJ2ZZsw9_E5R3YnzdCYFfcxjoR-rnHg.jpg │ ├── UNlogo.png │ ├── example.aux │ ├── example.docx │ ├── example.log │ ├── example.out │ ├── example.pdf │ ├── example.rtf │ ├── example.tex │ ├── example.toc │ ├── header.tex │ ├── in.html │ ├── main.tex │ ├── metadata.tex │ ├── post.json │ ├── pre.json │ ├── preprocessed.html │ └── template-metadata.tex ├── circle.yml ├── docker ├── libicu52_52.1-3_amd64.deb ├── pandoc-data_1.12.2.1-1build2_all.deb └── pandoc_1.12.2.1-1build2_amd64.deb ├── input └── .gitkeep ├── lib ├── include │ └── preprocess.rb ├── pandoc-filter.py └── pandoc-preprocess.rb └── spec ├── empty_headers_spec.rb ├── fixtures ├── sample-doc.html ├── simple-meta.html ├── simple-pagebreak.html ├── simple.html └── simple.md ├── headers_footers_spec.rb ├── image_parents_spec.rb ├── image_paths_spec.rb ├── nested_lists_spec.rb ├── page_breaks_spec.rb ├── span_styles_spec.rb ├── spec_helper.rb └── titles_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | *.swp 3 | build/* 4 | stuy/ 5 | input/* 6 | 7 | vendor 8 | .bundle/ 9 | Gemfile.lock 10 | 11 | google-api-authorization.yaml 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:trusty 2 | MAINTAINER Alex Dergachev 3 | 4 | # check if the docker host is running squid-deb-proxy, and use it 5 | RUN route -n | awk '/^0.0.0.0/ {print $2}' > /tmp/host_ip.txt 6 | RUN echo "HEAD /" | nc `cat /tmp/host_ip.txt` 8000 | grep squid-deb-proxy && (echo "Acquire::http::Proxy \"http://$(cat /tmp/host_ip.txt):8000\";" > /etc/apt/apt.conf.d/30proxy) || echo "No squid-deb-proxy detected" 7 | 8 | # install misc tools 9 | RUN apt-get update -y && apt-get install -y curl wget git fontconfig make vim 10 | 11 | RUN echo 'LC_ALL="en_US.UTF-8"' > /etc/default/locale 12 | RUN apt-get install -y ruby1.9.3 13 | 14 | # get pandocfilters, a helper library for writing pandoc filters in python 15 | RUN apt-get -y install python-pip 16 | RUN pip install pandocfilters 17 | 18 | # latex tools 19 | RUN apt-get update -y && apt-get install -y texlive-latex-base texlive-xetex latex-xcolor texlive-math-extra texlive-latex-extra texlive-fonts-extra rubber latexdiff 20 | 21 | # greatly speeds up nokogiri install 22 | # dependencies for nokogiri gem 23 | RUN apt-get install libxml2-dev libxslt1-dev pkg-config -y 24 | 25 | # install bundler 26 | RUN (gem list bundler | grep bundler) || gem install bundler 27 | 28 | # install gems 29 | ADD Gemfile /tmp/ 30 | ADD Gemfile.lock /tmp/ 31 | RUN cd /tmp && bundle config build.nokogiri --use-system-libraries && bundle install 32 | 33 | # install pandoc 1.12 by from manually downloaded trusty deb packages (saucy only has 1.11, which is too old) 34 | RUN apt-get install -y pandoc 35 | 36 | EXPOSE 12736 37 | WORKDIR /var/gdocs-export/ 38 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # later releases of google-api-client remove bin/google-api-client utility which we need 4 | gem "google-api-client", "0.6.4" 5 | gem "jwt", "~> 0.1.4" 6 | 7 | # for run.sh 8 | gem "thor" 9 | 10 | gem "nokogiri" 11 | 12 | gem "rspec", ">=3.1" 13 | gem "rspec_junit_formatter", :group => :development 14 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.4.0) 5 | autoparse (0.3.3) 6 | addressable (>= 2.3.1) 7 | extlib (>= 0.9.15) 8 | multi_json (>= 1.0.0) 9 | builder (3.2.2) 10 | diff-lcs (1.2.5) 11 | extlib (0.9.16) 12 | faraday (0.8.11) 13 | multipart-post (~> 1.2.0) 14 | google-api-client (0.6.4) 15 | addressable (>= 2.3.2) 16 | autoparse (>= 0.3.3) 17 | extlib (>= 0.9.15) 18 | faraday (~> 0.8.4) 19 | jwt (>= 0.1.5) 20 | launchy (>= 2.1.1) 21 | multi_json (>= 1.0.0) 22 | signet (~> 0.4.5) 23 | uuidtools (>= 2.1.0) 24 | jwt (0.1.13) 25 | multi_json (>= 1.5) 26 | launchy (2.4.3) 27 | addressable (~> 2.3) 28 | mini_portile2 (2.0.0) 29 | multi_json (1.11.2) 30 | multipart-post (1.2.0) 31 | nokogiri (1.6.7.1) 32 | mini_portile2 (~> 2.0.0.rc2) 33 | rspec (3.4.0) 34 | rspec-core (~> 3.4.0) 35 | rspec-expectations (~> 3.4.0) 36 | rspec-mocks (~> 3.4.0) 37 | rspec-core (3.4.1) 38 | rspec-support (~> 3.4.0) 39 | rspec-expectations (3.4.0) 40 | diff-lcs (>= 1.2.0, < 2.0) 41 | rspec-support (~> 3.4.0) 42 | rspec-mocks (3.4.1) 43 | diff-lcs (>= 1.2.0, < 2.0) 44 | rspec-support (~> 3.4.0) 45 | rspec-support (3.4.1) 46 | rspec_junit_formatter (0.2.3) 47 | builder (< 4) 48 | rspec-core (>= 2, < 4, != 2.12.0) 49 | signet (0.4.5) 50 | addressable (>= 2.2.3) 51 | faraday (~> 0.8.1) 52 | jwt (>= 0.1.5) 53 | multi_json (>= 1.0.0) 54 | thor (0.19.1) 55 | uuidtools (2.1.5) 56 | 57 | PLATFORMS 58 | ruby 59 | 60 | DEPENDENCIES 61 | google-api-client (= 0.6.4) 62 | jwt (~> 0.1.4) 63 | nokogiri 64 | rspec (>= 3.1) 65 | rspec_junit_formatter 66 | thor 67 | 68 | BUNDLED WITH 69 | 1.15.4 70 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #=============================================================================== 2 | # DEFAULT MAKE VARIABLES 3 | #=============================================================================== 4 | 5 | # defaults to "Test doc for gd-pandoc" 6 | doc = https://docs.google.com/a/evolvingweb.ca/document/d/1dwYaiiy4P0KA7PvNwAP2fsPAf6qMMNzwaq8W66mwyds/edit#heading=h.4lk08p1hx3w 7 | 8 | outdir=build 9 | doc_id = $(shell echo $(doc) | sed -e 's@^https.*document/d/@@' -e 's@/edit.*@@') 10 | name = default 11 | input_file = input/$(name).html 12 | OUTPUT=$(outdir)/$(name) 13 | auth_file = google-api-authorization.yaml 14 | docker_workdir=/var/gdocs-export/ 15 | docker_run_cmd = docker run -t -i -v `pwd`:$(docker_workdir) -p 12736:12736 dergachev/gdocs-export 16 | 17 | # directory containing customized header.tex, etc... 18 | theme = sample 19 | 20 | all: convert 21 | 22 | #=============================================================================== 23 | # GOOGLE_DRIVE_API TARGETS 24 | #=============================================================================== 25 | 26 | install_auth_file: 27 | cp $(workdir)$(auth_file) ~/.google-api.yaml 28 | 29 | api_auth: 30 | bundle exec ruby bin/authorize.rb \ 31 | $(CLIENT_ID) $(CLIENT_SECRET) \ 32 | https://www.googleapis.com/auth/drive.readonly \ 33 | > $(auth_file) 34 | 35 | api_download: install_auth_file 36 | bundle exec google-api execute \ 37 | -u "https://docs.google.com/feeds/download/documents/export/Export?id=$(doc_id)&exportFormat=html" \ 38 | > $(input_file) 39 | 40 | #=============================================================================== 41 | # PANDOC TARGETS 42 | #=============================================================================== 43 | 44 | latex: 45 | mkdir -p $(OUTPUT) 46 | cp assets/default/* $(OUTPUT) 47 | test -z "$(theme)" || cp assets/$(theme)/* $(OUTPUT) 48 | cp $(input_file) $(OUTPUT)/in.html 49 | 50 | bundle exec ruby -C$(OUTPUT) "$$PWD/lib/pandoc-preprocess.rb" in.html > $(OUTPUT)/preprocessed.html 51 | pandoc --parse-raw $(OUTPUT)/preprocessed.html -t json > $(OUTPUT)/pre.json 52 | cat $(OUTPUT)/pre.json | ./lib/pandoc-filter.py > $(OUTPUT)/post.json 53 | 54 | # use pandoc to create metadata.tex, main.tex (these are included by ew-template.tex) 55 | pandoc $(OUTPUT)/post.json --no-wrap -t latex --template $(OUTPUT)/template-metadata.tex > $(OUTPUT)/metadata.tex 56 | pandoc $(OUTPUT)/post.json --chapters --no-wrap -t latex > $(OUTPUT)/main.tex 57 | 58 | # must use -o with docx output format, since its binary 59 | pandoc $(OUTPUT)/post.json -s -t docx -o $(OUTPUT)/$(name).docx 60 | pandoc $(OUTPUT)/post.json -s -t rtf -o $(OUTPUT)/$(name).rtf 61 | 62 | pdf: 63 | # convert latex to PDF 64 | echo "Created $(OUTPUT)/$(name).tex, compiling into $(name).pdf" 65 | # rubber will set output PDF filename based on latex input filename 66 | cp -f $(OUTPUT)/template.tex $(OUTPUT)/$(name).tex 67 | ( cd $(OUTPUT); latexmk -pdf $(name)) 68 | 69 | convert: latex pdf 70 | 71 | diff: 72 | /usr/bin/perl "`which latexdiff`" --flatten $(outdir)/$(before)/$(before).tex $(OUTPUT)/$(name).tex > $(OUTPUT)/diff.tex 73 | (cd $(OUTPUT); latexmk -pdf diff) 74 | 75 | 76 | #=============================================================================== 77 | # DOCKER TARGETS 78 | #=============================================================================== 79 | 80 | build_docker: 81 | @echo "Warning: building can take a while (~15m)." 82 | dpkg -l squid-deb-proxy || sudo apt-get install -y squid-deb-proxy 83 | docker build -t dergachev/gdocs-export . 84 | 85 | docker_debug: 86 | $(docker_run_cmd) /bin/bash 87 | 88 | latest: 89 | docker run -t -i `docker images -q | head -n 1` /bin/bash 90 | 91 | docker_api_auth: 92 | $(docker_run_cmd) make api_auth CLIENT_ID=$(CLIENT_ID) CLIENT_SECRET=$(CLIENT_SECRET) 93 | 94 | docker_api_download: 95 | $(docker_run_cmd) make api_download doc_id=$(doc_id) input_file=$(input_file) workdir=$(docker_workdir) 96 | 97 | docker_convert: 98 | $(docker_run_cmd) make convert OUTPUT=$(OUTPUT) name=$(name) input_file=$(input_file) theme=$(theme) 99 | 100 | docker_diff: 101 | docker run -t -i -v `pwd`:$(docker_workdir) -p 12736:12736 dergachev/gdocs-export make diff OUTPUT=$(OUTPUT) name=$(name) input_file=$(input_file) before=$(before) 102 | 103 | #=============================================================================== 104 | # MISC TARGETS 105 | #=============================================================================== 106 | 107 | test: 108 | bundle exec rspec 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gdocs-export 2 | ============ 3 | 4 | Script to programatically download a text document from Google Docs and convert 5 | it to LaTeX, and compile it into a PDF. 6 | 7 | For example, this [google 8 | doc](https://docs.google.com/a/evolvingweb.ca/document/d/1dwYaiiy4P0KA7PvNwAP2fsPAf6qMMNzwaq8W66mwyds/edit) 9 | is converted into [this 10 | pdf](https://raw.githubusercontent.com/dergachev/gdocs-export/master/build/example/example.pdf). 11 | 12 | Under the hood, it uses [pandoc](http://johnmacfarlane.net/pandoc/) to convert from HTML to LaTeX and PDF. 13 | 14 | Installation 15 | ------------ 16 | 17 | See below for how to get google drive API *client_id* and *client_secret*. 18 | See the `Vagrantfile` for installation steps. 19 | See the `Makefile` for usage. 20 | 21 | Starts a VM with docker and squid-deb-proxy running, then builds the gdocs-export docker image: 22 | 23 | ```bash 24 | vagrant up 25 | vagrant ssh 26 | cd /vagrant/ 27 | 28 | # pulls the image from index.docker.io (about ~2GB) 29 | docker pull dergachev/gdocs-export 30 | ``` 31 | 32 | Alternatively, we can build the image from this repo, but this takes a while 33 | and installing squid-deb-proxy to cache 'apt-get install' downloads is highly 34 | recommended: 35 | 36 | ```bash 37 | # optional, caches apt-get downloads in containers 38 | apt-get install -y squid-deb-proxy 39 | 40 | # takes 10-20 minutes 41 | docker build -t dergachev/gdocs-export . 42 | ``` 43 | 44 | To run the tests, do the following: 45 | 46 | ```bash 47 | bundle config build.nokogiri --use-system-libraries 48 | bundle install 49 | 50 | make test 51 | ``` 52 | 53 | Configuration 54 | ------------- 55 | 56 | Before being able to interact with Google APIs, you'll need to register a new 57 | project in the [Google Developers 58 | Console](https://console.developers.google.com/project), enable Google Drive 59 | SDK for it, then retreive the associated *client_id* and *client_secret* 60 | parameters. For help with this, see below. 61 | 62 | ```bash 63 | # see below for how to get your Google API client_id and client_secret (these are fake) 64 | export CLIENT_ID=409429960585-o2i6nc17gf5sdhpa6o2g5gkkclmq229g.apps.googleusercontent.com 65 | export CLIENT_SECRET=PqKk00otoY11cxEfSSE7pCdw 66 | ``` 67 | 68 | Now you'll need to give our newly-registered app permission to read all the Google Drive 69 | documents associated with your account, which is done via an in-browser OATH workflow. 70 | 71 | Before gdocs-export can download Google Drive documents associated with your 72 | account, you'll need to grant it the appropriate permissions. 73 | 74 | 75 | ```bash 76 | make docker_api_auth CLIENT_ID=$CLIENT_ID CLIENT_SECRET=$CLIENT_SECRET 77 | ``` 78 | 79 | That command command will prompt you to visit a URL that looks like this: 80 | 81 | ``` 82 | https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force&client_id=CLIENT_ID_GOES_HERE&redirect_uri=http://localhost:12736/&response_type=code&scope=https://www.googleapis.com/auth/drive.readonly 83 | ``` 84 | 85 | On successful authorization, the browser will be automatically redirected to 86 | http://localhost:12736, where the command is listening for the resulting access 87 | tokens, which it will save to `./google-api-authorization.yaml` in the 88 | following format: 89 | 90 | ``` 91 | --- 92 | mechanism: oauth_2 93 | scope: https://www.googleapis.com/auth/drive.readonly 94 | client_id: 409429960585-o2i6nc17gf5sdhpa6o2g5gkkclmq229g.apps.googleusercontent.com 95 | client_secret: PqKk00otoY11cxEfSSE7pCdw 96 | access_token: ya29.1.klsfj3kj3kj23k4jkkjsfkfjksdjfksdjfkjjiuiquiuwiue-324k234kj324GI 97 | refresh_token: 1/EJKERKJERKJ3jkkj34998889i9jkAAAAAAjjjjjjjzQ 98 | ``` 99 | 100 | The *client_id* and *client_secret* properties are your application's API 101 | credentials, while the *access_token* and *refresh_token* are proof that your 102 | application has been authorized access to a given account's data. Be sure to 103 | keep this file private! 104 | 105 | Usage 106 | ----- 107 | 108 | Now that we've got all the access tokens we need, we can use it to download an 109 | arbitrary document. For example, try downloading the [gdocs-export example 110 | document](https://docs.google.com/a/evolvingweb.ca/document/d/1dwYaiiy4P0KA7PvNwAP2fsPAf6qMMNzwaq8W66mwyds/edit) 111 | which I've shared publicly. 112 | 113 | ```bash 114 | export GOOGLE_DOC_URL=https://docs.google.com/a/evolvingweb.ca/document/d/1dwYaiiy4P0KA7PvNwAP2fsPAf6qMMNzwaq8W66mwyds/edit 115 | make docker_api_download name=example doc=$GOOGLE_DOC_URL 116 | ``` 117 | 118 | The above just created `./input/example.html`. Let's convert it to PDF: 119 | 120 | ```bash 121 | make docker_convert name=example 122 | ``` 123 | 124 | The above command creates the following files inside of `./build/example/`: 125 | 126 | example.pdf 127 | example.docx 128 | example.rtf 129 | 130 | By default it'll use the header.tex and logo image in `./assets/sample/`. 131 | To use the customized files under `./assets/ew/` instead, do the following: 132 | 133 | ```bash 134 | # pick up latex assets from ./assets/ew/ instead of ./assets/sample 135 | make docker_convert name=example theme=ew 136 | ``` 137 | 138 | Finally, we also support generating a diff.pdf highlighting differences between 139 | the current document and a previously downloaded and compiled version. The 140 | workflow is as follows: 141 | 142 | ```bash 143 | make docker_api_download name=my-doc-v1 doc=$GOOGLE_DOC_URL 144 | make docker_convert name=my-doc-v1 145 | 146 | # make some changes to the document 147 | make docker_api_download name=my-doc-v2 doc=$GOOGLE_DOC_URL 148 | make docker_convert name=my-doc-v2 149 | 150 | # creates build/my-doc-v2/diff.pdf 151 | make docker_diff before=my-doc-v1 name=my-doc-v2 152 | ``` 153 | 154 | Registering with Google Developers Console 155 | ------------------------------------------ 156 | 157 | The following shows how to get a register your app (or rather, your instance of 158 | gdocs-export) and get a Google API *client_id* and *client_secret* tokens. 159 | 160 | The Google Developers Console API console seems to be always changing. The 161 | following steps were sufficient as of March 4, 2014. 162 | 163 | * Visit https://console.developers.google.com/project, create new project (pick a descriptive name and ID) 164 | ![](https://dl.dropbox.com/u/29440342/screenshots/QOXZHZMW-2014.03.04-17-49-16.png) 165 | * In the new project, go to "APIs & Auths > APIs" and enable "Drive SDK". 166 | * Leave defaults, since they seem to include http://localhost (which covers http://localhost:12736) 167 | ![](https://dl.dropbox.com/u/29440342/screenshots/YXQGJYLR-2014.03.04-17-50-44.png) 168 | * Visit "APIs & Auths > Credentials" and click "Create New Client ID" 169 | ![](https://dl.dropbox.com/u/29440342/screenshots/QJRSROZL-2014.03.04-17-51-50.png) 170 | * Select "Installed Application" and "Other" when prompted for application type. 171 | ![](https://dl.dropbox.com/u/29440342/screenshots/SNFDZSWW-2014.03.04-17-52-19.png) 172 | * Copy "Client ID" and "Client Secret", store them somewhere for future sourcing. 173 | ![](https://dl.dropbox.com/u/29440342/screenshots/GGJDQSIN-2014.03.04-17-57-34.png) 174 | 175 | Installing on Mac 176 | ----------------- 177 | 178 | ``` 179 | sudo tlmgr latex-diff 180 | sudo tlmgr install latexmk 181 | ``` 182 | 183 | See http://mg.readthedocs.io/latexmk.html 184 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "raring64" 6 | config.vm.box_url = "http://cloud-images.ubuntu.com/vagrant/raring/current/raring-server-cloudimg-amd64-vagrant-disk1.box" 7 | config.ssh.forward_agent = true 8 | 9 | # building pandoc needs a lot of RAM (at least 512, the more the better) 10 | config.vm.provider :virtualbox do |vb| 11 | vb.customize ["modifyvm", :id, "--memory", "2048"] 12 | vb.customize ["modifyvm", :id, "--cpus", "2"] 13 | end 14 | 15 | # required for lib/authorize.rb 16 | config.vm.network "forwarded_port", guest: 12736, host: 12736 17 | 18 | if defined? VagrantPlugins::Cachier 19 | config.cache.auto_detect = true 20 | end 21 | 22 | config.vm.provision "docker" 23 | 24 | config.vm.provision :shell, :inline => <<-EOT 25 | # our docker containers expect the host to have squid-deb-proxy 26 | apt-get install squid-deb-proxy -y 27 | apt-get update 28 | 29 | # standard utilities 30 | apt-get install -y build-essential vim curl git 31 | EOT 32 | end 33 | 34 | -------------------------------------------------------------------------------- /assets/default/template-metadata.tex: -------------------------------------------------------------------------------- 1 | \newcommand{\mytitle}{$title$} 2 | \newcommand{\mysubtitle}{$subtitle$} 3 | \newcommand{\mybody}{$body$} 4 | 5 | $if(header)$ 6 | \newcommand{\myheader}{$header$} 7 | $endif$ 8 | 9 | $if(footer)$ 10 | \newcommand{\myfooter}{$footer$} 11 | $endif$ 12 | 13 | % TODO: support table of key-value metadata in google doc 14 | -------------------------------------------------------------------------------- /assets/default/template.tex: -------------------------------------------------------------------------------- 1 | % Docs in http://www.tex.ac.uk/ctan/macros/latex/contrib/memoir/memman.pdf 2 | 3 | \documentclass[letterpaper,oldfontcommands,11pt,extrafontsizes,onecolumn,oneside,openboth,final,titlepage]{memoir} 4 | \usepackage[T1]{fontenc} 5 | \usepackage{lmodern} 6 | \usepackage{amssymb,amsmath} 7 | \usepackage{ifxetex,ifluatex} 8 | \usepackage{fixltx2e} % provides \textsubscript 9 | \usepackage[dvipsnames]{xcolor} % Allows the definition of hex colors 10 | % use microtype if available 11 | \IfFileExists{microtype.sty}{\usepackage{microtype}}{} 12 | % use upquote if available, for straight quotes in verbatim environments 13 | \IfFileExists{upquote.sty}{\usepackage{upquote}}{} 14 | \usepackage[utf8]{inputenc} 15 | \usepackage{eurosym} 16 | % used for printing google docs templates 17 | \usepackage{longtable} 18 | % used for printing google docs images 19 | \usepackage{graphicx} 20 | % to support underlining, see http://tex.stackexchange.com/questions/13377/how-can-i-underline-a-single-word-with-latex 21 | \usepackage[normalem]{ulem} 22 | % used for google docs links 23 | \usepackage[unicode=true]{hyperref} 24 | 25 | \input{metadata.tex} 26 | 27 | \hypersetup{breaklinks=true, 28 | bookmarks=true, 29 | pdfauthor=, %TODO: fix the author 30 | pdftitle=\mytitle, 31 | colorlinks=true, 32 | urlcolor=blue, 33 | linkcolor=magenta, 34 | pdfborder={0 0 0}} 35 | 36 | % reduce page margins to a reasonable size 37 | \setlrmarginsandblock{2.5cm}{2.5cm}{*} 38 | \setulmarginsandblock{2.5cm}{2.5cm}{*} 39 | \setlength{\footskip}{1.2cm} 40 | \checkandfixthelayout 41 | 42 | % customize header and footer 43 | \nouppercaseheads 44 | \makepagestyle{mystyle} 45 | \pagestyle{mystyle} 46 | \aliaspagestyle{chapter}{mystyle} 47 | 48 | \ifdefined\myheader 49 | \makeheadrule{mystyle}{\textwidth}{0.5pt} 50 | \makeoddhead{mystyle}{}{}{\color{Gray}\itshape\myheader} 51 | \fi 52 | 53 | \ifdefined\myfooter 54 | \makeoddfoot{mystyle}{\myfooter}{}{\thepage/\thelastpage} 55 | \else 56 | \makeoddfoot{mystyle}{}{}{\thepage/\thelastpage} 57 | \fi 58 | \makefootrule{mystyle}{\textwidth}{0.5pt}{0ex} 59 | 60 | \makeheadfootruleprefix{mystyle}{\color{Gray}}{\color{Gray}} 61 | 62 | \chapterstyle{komalike} % also consider article 63 | 64 | % We will generate all images so they have a width \maxwidth. This means 65 | % that they will get their normal width if they fit onto the page, but 66 | % are scaled down if they would overflow the margins. 67 | \makeatletter 68 | \def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth 69 | \else\Gin@nat@width\fi} 70 | \makeatother 71 | \let\Oldincludegraphics\includegraphics 72 | \renewcommand{\includegraphics}[1]{\Oldincludegraphics[width=\maxwidth]{#1}} 73 | 74 | % Uncomment to make embedded links print out HREF in footnotes, good for printing 75 | % \renewcommand{\href}[2]{#2\footnote{\url{#1}}} 76 | 77 | \setlength{\parindent}{0pt} 78 | \setlength{\parskip}{7pt plus 2pt minus 1pt} 79 | \setlength{\emergencystretch}{3em} % prevent overfull lines 80 | 81 | % Limits depth of automatic numbering of headings. 82 | % 0 => Only H1 (chapters), 1 => Up to H2 (sections), 2 => Up to H3 (subsections) 83 | \setcounter{secnumdepth}{2} 84 | 85 | % memoir specific variable that lowers the title by 200pt 86 | \setlength{\droptitle}{200pt} 87 | 88 | % customize size and alignment of title and subtitle 89 | \pretitle{\begin{flushleft}} 90 | \title{\bfseries \HUGE \mytitle \\ \vskip .5em \LARGE \mysubtitle} 91 | \posttitle{\par\vskip1em{\normalfont\normalsize\scshape \par\vfill}\end{flushleft}\vskip 0.5em} 92 | 93 | % TODO generate the author somehow 94 | % \author{} 95 | 96 | % Suppress printing todays date. 97 | % TODO: currently looks bad if not blank; make it look good and replace with todays date 98 | \date{} 99 | 100 | \begin{document} 101 | 102 | \input{header.tex} 103 | 104 | \setcounter{tocdepth}{1} 105 | \begin{KeepFromToc} 106 | \hypersetup{linkcolor=black} 107 | \tableofcontents 108 | \end{KeepFromToc} 109 | \pagebreak 110 | 111 | \input{main.tex} 112 | 113 | \end{document} 114 | -------------------------------------------------------------------------------- /assets/ew/ew-logo-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergachev/gdocs-export/df691eb51646026de0269c2942814afc3dea190c/assets/ew/ew-logo-large.png -------------------------------------------------------------------------------- /assets/ew/header.tex: -------------------------------------------------------------------------------- 1 | \begin{titlingpage} 2 | \maketitle 3 | \vfill 4 | \Oldincludegraphics[width=3in]{./ew-logo-large.png} 5 | \end{titlingpage} 6 | 7 | \pagebreak 8 | 9 | % suppress headers on this page 10 | \thispagestyle{empty} 11 | 12 | \emph{Prepared by} \textbf{Evolving Web Inc.} \\ 13 | 300, rue du Saint-Sacrement --- Suite 222 \\ 14 | Montreal, QC H2Y 1X4 \\ 15 | phone: (514) 844-4930 \\ 16 | fax: (514) 807-7499 \\ 17 | \href{http://evolvingweb.ca}{http://evolvingweb.ca} 18 | 19 | \vfill 20 | \textbf{CONFIDENTIAL}:~The contents of this document are confidential 21 | and are intended exclusively for the prospective customer of Evolving 22 | Web designated above and its employees. Distribution of or sharing of 23 | this information with persons or entities for which it is not intended 24 | is prohibited, in any form, without the express written consent of 25 | Evolving Web. 26 | \pagebreak 27 | -------------------------------------------------------------------------------- /assets/sample/UNlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergachev/gdocs-export/df691eb51646026de0269c2942814afc3dea190c/assets/sample/UNlogo.png -------------------------------------------------------------------------------- /assets/sample/header.tex: -------------------------------------------------------------------------------- 1 | \begin{titlingpage} 2 | \maketitle 3 | \vfill 4 | \Oldincludegraphics[width=2.5in]{./UNlogo.png} 5 | \end{titlingpage} 6 | 7 | \pagebreak 8 | 9 | % suppress headers on this page 10 | \thispagestyle{empty} 11 | 12 | \textbf{Prepared by:} 13 | 14 | Vendor Inc.,\\49 Seven square\\Montreal, QC H0H 0H0\\phone: (514) 15 | 345-6789\\fax: (514) 345-6789\\http://vendor.com 16 | 17 | \textbf{} 18 | 19 | \textbf{Prepared for:} 20 | 21 | Client LLC,\\37 Prime Lane\\Montreal, QC H0H 0H0\\phone: (514) 22 | 345-6789\\fax: (514) 345-6789\\http://supplier.com 23 | 24 | \vfill 25 | 26 | CONFIDENTIAL: The contents of this document are confidential and are 27 | intended exclusively for the designated recipients. The contents of this page 28 | is defined in a file called header.tex. Other legal mumbo jumbo\ldots{}. 29 | 30 | \pagebreak 31 | -------------------------------------------------------------------------------- /bin/authorize.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | OAUTH_SERVER_PORT = 12736 4 | 5 | require 'signet/oauth_2/client' 6 | require 'webrick' 7 | require 'yaml' 8 | 9 | ARGV.unshift('--help') if ARGV.empty? 10 | 11 | module Google 12 | class InstalledAppFlow 13 | def initialize(options) 14 | @port = options[:port] || 9292 15 | @authorization = Signet::OAuth2::Client.new({ 16 | :authorization_uri => 'https://accounts.google.com/o/oauth2/auth', 17 | :token_credential_uri => 'https://accounts.google.com/o/oauth2/token', 18 | :redirect_uri => "http://localhost:#{@port}/"}.update(options) 19 | ) 20 | end 21 | 22 | def authorize 23 | auth = @authorization 24 | 25 | server = WEBrick::HTTPServer.new( 26 | :Port => @port, 27 | :BindAddress =>"0.0.0.0", 28 | :Logger => WEBrick::Log.new(STDERR, 0), 29 | :AccessLog => [] 30 | ) 31 | trap("INT") { server.shutdown } 32 | 33 | server.mount_proc '/' do |req, res| 34 | auth.code = req.query['code'] 35 | if auth.code 36 | auth.fetch_access_token! 37 | end 38 | res.status = WEBrick::HTTPStatus::RC_ACCEPTED 39 | res.body = <<-HTML 40 | 41 | 42 | 49 | 50 | You may close this window. 51 | 52 | HTML 53 | 54 | server.stop 55 | end 56 | 57 | STDERR.puts "Visit the following URL to authorize:\n #{auth.authorization_uri.to_s}" 58 | server.start 59 | if @authorization.access_token 60 | return @authorization 61 | else 62 | return nil 63 | end 64 | end 65 | end 66 | 67 | class CLI 68 | def self.oauth_2_login(options) 69 | flow = Google::InstalledAppFlow.new( 70 | :port => OAUTH_SERVER_PORT, 71 | :client_id => options[:client_credential_key], 72 | :client_secret => options[:client_credential_secret], 73 | :scope => options[:scope] 74 | ) 75 | 76 | oauth_client = flow.authorize 77 | if oauth_client 78 | config = { 79 | "mechanism" => "oauth_2", 80 | "scope" => options[:scope], 81 | "client_id" => oauth_client.client_id, 82 | "client_secret" => oauth_client.client_secret, 83 | "access_token" => oauth_client.access_token, 84 | "refresh_token" => oauth_client.refresh_token 85 | } 86 | # AUTHFILE_YAML_PATH = 'google-api-authorization.yaml' 87 | # config_file = File.expand_path(AUTHFILE_YAML_PATH) 88 | # open(config_file, 'w') { |file| file.write(YAML.dump(config)) } 89 | STDERR.puts "Success! Received the following credentials (also printing them to STDOUT):" 90 | STDERR.puts YAML.dump(config) 91 | STDOUT.puts YAML.dump(config) 92 | 93 | end 94 | exit(0) 95 | end 96 | end 97 | end 98 | 99 | Google::CLI::oauth_2_login({ :client_credential_key => ARGV[0], :client_credential_secret => ARGV[1], :scope => ARGV[2] }) 100 | -------------------------------------------------------------------------------- /build/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergachev/gdocs-export/df691eb51646026de0269c2942814afc3dea190c/build/.gitkeep -------------------------------------------------------------------------------- /build/example/SHbO80B3zYiLwN09wcE1jxTyLWaBsr7llKXblVz0EwCQsT2EsGznwPa4YwxgN3BwyMSEZhyazyYEoA5TNEBswNe6FJ2ZZsw9_E5R3YnzdCYFfcxjoR-rnHg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergachev/gdocs-export/df691eb51646026de0269c2942814afc3dea190c/build/example/SHbO80B3zYiLwN09wcE1jxTyLWaBsr7llKXblVz0EwCQsT2EsGznwPa4YwxgN3BwyMSEZhyazyYEoA5TNEBswNe6FJ2ZZsw9_E5R3YnzdCYFfcxjoR-rnHg.jpg -------------------------------------------------------------------------------- /build/example/UNlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergachev/gdocs-export/df691eb51646026de0269c2942814afc3dea190c/build/example/UNlogo.png -------------------------------------------------------------------------------- /build/example/example.aux: -------------------------------------------------------------------------------- 1 | \relax 2 | \providecommand\hyper@newdestlabel[2]{} 3 | \providecommand*{\memsetcounter}[2]{} 4 | \providecommand\HyperFirstAtBeginDocument{\AtBeginDocument} 5 | \HyperFirstAtBeginDocument{\ifx\hyper@anchor\@undefined 6 | \global\let\oldcontentsline\contentsline 7 | \gdef\contentsline#1#2#3#4{\oldcontentsline{#1}{#2}{#3}} 8 | \global\let\oldnewlabel\newlabel 9 | \gdef\newlabel#1#2{\newlabelxx{#1}#2} 10 | \gdef\newlabelxx#1#2#3#4#5#6{\oldnewlabel{#1}{{#2}{#3}}} 11 | \AtEndDocument{\ifx\hyper@anchor\@undefined 12 | \let\contentsline\oldcontentsline 13 | \let\newlabel\oldnewlabel 14 | \fi} 15 | \fi} 16 | \global\let\hyper@last\relax 17 | \gdef\HyperFirstAtBeginDocument#1{#1} 18 | \providecommand\HyField@AuxAddToFields[1]{} 19 | \providecommand\HyField@AuxAddToCoFields[2]{} 20 | \@writefile{lof}{\addvspace {10pt}} 21 | \@writefile{lot}{\addvspace {10pt}} 22 | \@writefile{toc}{\contentsline {chapter}{\chapternumberline {1}Background}{3}{chapter.1}} 23 | \@writefile{lof}{\addvspace {10pt}} 24 | \@writefile{lot}{\addvspace {10pt}} 25 | \@writefile{toc}{\contentsline {chapter}{\chapternumberline {2}Proposed Solution}{4}{chapter.2}} 26 | \@writefile{toc}{\contentsline {section}{\numberline {2.1}Export component}{4}{section.2.1}} 27 | \@writefile{toc}{\contentsline {section}{\numberline {2.2}Import component}{4}{section.2.2}} 28 | \@writefile{toc}{\contentsline {section}{\numberline {2.3}Benefits of our solution}{4}{section.2.3}} 29 | \gdef \LT@i {\LT@entry 30 | {1}{227.84059pt}\LT@entry 31 | {1}{227.84059pt}} 32 | \@writefile{lof}{\addvspace {10pt}} 33 | \@writefile{lot}{\addvspace {10pt}} 34 | \@writefile{toc}{\contentsline {chapter}{\chapternumberline {3}Schedule and Budget}{6}{chapter.3}} 35 | \@writefile{toc}{\contentsline {section}{\numberline {3.1}Table}{6}{section.3.1}} 36 | \@writefile{toc}{\contentsline {section}{\numberline {3.2}Diagram showing how work is split}{7}{section.3.2}} 37 | \@writefile{lof}{\addvspace {10pt}} 38 | \@writefile{lot}{\addvspace {10pt}} 39 | \@writefile{toc}{\contentsline {chapter}{\chapternumberline {4}Terms and Conditions}{8}{chapter.4}} 40 | \@writefile{toc}{\contentsline {section}{\numberline {4.1}Staffing}{8}{section.4.1}} 41 | \@writefile{toc}{\contentsline {section}{\numberline {4.2}Relation to Other Agreements}{8}{section.4.2}} 42 | \@writefile{toc}{\contentsline {section}{\numberline {4.3}Language of Contract}{8}{section.4.3}} 43 | \@writefile{lof}{\addvspace {10pt}} 44 | \@writefile{lot}{\addvspace {10pt}} 45 | \@writefile{toc}{\contentsline {chapter}{\chapternumberline {5}Agreement}{9}{chapter.5}} 46 | \memsetcounter{lastsheet}{10} 47 | \memsetcounter{lastpage}{9} 48 | -------------------------------------------------------------------------------- /build/example/example.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergachev/gdocs-export/df691eb51646026de0269c2942814afc3dea190c/build/example/example.docx -------------------------------------------------------------------------------- /build/example/example.out: -------------------------------------------------------------------------------- 1 | \BOOKMARK [0][-]{chapter.1}{\376\377\000B\000a\000c\000k\000g\000r\000o\000u\000n\000d}{}% 1 2 | \BOOKMARK [0][-]{chapter.2}{\376\377\000P\000r\000o\000p\000o\000s\000e\000d\000\040\000S\000o\000l\000u\000t\000i\000o\000n}{}% 2 3 | \BOOKMARK [1][-]{section.2.1}{\376\377\000E\000x\000p\000o\000r\000t\000\040\000c\000o\000m\000p\000o\000n\000e\000n\000t}{chapter.2}% 3 4 | \BOOKMARK [1][-]{section.2.2}{\376\377\000I\000m\000p\000o\000r\000t\000\040\000c\000o\000m\000p\000o\000n\000e\000n\000t}{chapter.2}% 4 5 | \BOOKMARK [1][-]{section.2.3}{\376\377\000B\000e\000n\000e\000f\000i\000t\000s\000\040\000o\000f\000\040\000o\000u\000r\000\040\000s\000o\000l\000u\000t\000i\000o\000n}{chapter.2}% 5 6 | \BOOKMARK [0][-]{chapter.3}{\376\377\000S\000c\000h\000e\000d\000u\000l\000e\000\040\000a\000n\000d\000\040\000B\000u\000d\000g\000e\000t}{}% 6 7 | \BOOKMARK [1][-]{section.3.1}{\376\377\000T\000a\000b\000l\000e}{chapter.3}% 7 8 | \BOOKMARK [1][-]{section.3.2}{\376\377\000D\000i\000a\000g\000r\000a\000m\000\040\000s\000h\000o\000w\000i\000n\000g\000\040\000h\000o\000w\000\040\000w\000o\000r\000k\000\040\000i\000s\000\040\000s\000p\000l\000i\000t}{chapter.3}% 8 9 | \BOOKMARK [0][-]{chapter.4}{\376\377\000T\000e\000r\000m\000s\000\040\000a\000n\000d\000\040\000C\000o\000n\000d\000i\000t\000i\000o\000n\000s}{}% 9 10 | \BOOKMARK [1][-]{section.4.1}{\376\377\000S\000t\000a\000f\000f\000i\000n\000g}{chapter.4}% 10 11 | \BOOKMARK [1][-]{section.4.2}{\376\377\000R\000e\000l\000a\000t\000i\000o\000n\000\040\000t\000o\000\040\000O\000t\000h\000e\000r\000\040\000A\000g\000r\000e\000e\000m\000e\000n\000t\000s}{chapter.4}% 11 12 | \BOOKMARK [1][-]{section.4.3}{\376\377\000L\000a\000n\000g\000u\000a\000g\000e\000\040\000o\000f\000\040\000C\000o\000n\000t\000r\000a\000c\000t}{chapter.4}% 12 13 | \BOOKMARK [0][-]{chapter.5}{\376\377\000A\000g\000r\000e\000e\000m\000e\000n\000t}{}% 13 14 | -------------------------------------------------------------------------------- /build/example/example.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergachev/gdocs-export/df691eb51646026de0269c2942814afc3dea190c/build/example/example.pdf -------------------------------------------------------------------------------- /build/example/example.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\deff0{\fonttbl{\f0 \fswiss Helvetica;}{\f1 Courier;}} 2 | {\colortbl;\red255\green0\blue0;\red0\green0\blue255;} 3 | \widowctrl\hyphauto 4 | 5 | {\pard \qc \f0 \sa180 \li0 \fi0 \b \fs36 Sample Statement of Work\par} 6 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 7 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 8 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 9 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 10 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 11 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 12 | {\pard \ql \f0 \sa180 \li0 \fi0 \b \fs36 Background\par} 13 | {\pard \ql \f0 \sa180 \li0 \fi0 Every project needs a background. Here\u8217's some lorem ipsum about the project.\par} 14 | {\pard \ql \f0 \sa180 \li0 \fi0 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sit amet arcu elit. Donec at massa et leo auctor fringilla vitae id erat. Nam feugiat velit vitae ornare tristique. Morbi porttitor, orci vel dapibus malesuada, magna libero ultrices massa, nec interdum diam nisi elementum ipsum. Vestibulum elit lorem, pretium id semper ac, aliquam ut dui. Etiam at magna non nibh viverra tristique. Maecenas consectetur vulputate tellus, ac rutrum felis molestie ut. Aenean tincidunt congue cursus. Maecenas in egestas magna, nec pellentesque justo. Phasellus ante quam, faucibus sit amet tellus et, fringilla convallis eros.\par} 15 | {\pard \ql \f0 \sa180 \li0 \fi0 Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra.\par} 16 | {\pard \ql \f0 \sa180 \li0 \fi0 Nunc at odio massa. In hac habitasse platea dictumst. Donec posuere erat ac ultrices suscipit. Quisque id velit nulla. Ut in urna orci. Sed at euismod neque. Sed velit velit, fringilla vitae eleifend eget, auctor id mi.\par} 17 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 18 | {\pard \ql \f0 \sa180 \li0 \fi0 \b \fs36 Proposed Solution\par} 19 | {\pard \ql \f0 \sa180 \li0 \fi0 This wouldn\u8217't be a proposal unless it proposed something. We will re-architect the system into a series of robust robust components:\par} 20 | {\pard \ql \f0 \sa0 \li360 \fi-360 1.\tx360\tab export, a script to export the data from access, validate it, refactor it into a logical format, then output it in plain-text format.\par} 21 | {\pard \ql \f0 \sa0 \li360 \fi-360 2.\tx360\tab import, a simple, robust module to handle file uploads of the cleaned data, and efficiently import this data. It will do sufficient error handling to allow full automation.\sa180\par} 22 | {\pard \ql \f0 \sa180 \li0 \fi0 As we perform the refactoring, we will rewriting the code to ensure it\u8217's documented, easy to test, and easy to update as future needs are identified.\par} 23 | {\pard \ql \f0 \sa180 \li0 \fi0 \b \fs32 Export component\par} 24 | {\pard \ql \f0 \sa0 \li360 \fi-360 \bullet \tx360\tab This will be a standalone script responsible for extracting data from source database, and cleaning it up as appropriate.\par} 25 | {\pard \ql \f0 \sa0 \li360 \fi-360 \bullet \tx360\tab The output format is human readable JSON files, with a logical, easy to understand structure\par} 26 | {\pard \ql \f0 \sa0 \li360 \fi-360 \bullet \tx360\tab Easy to verify output through manual inspection of output\par} 27 | {\pard \ql \f0 \sa0 \li360 \fi-360 \bullet \tx360\tab Include comprehensive automated tests to ensure output is correct\sa180\par} 28 | {\pard \ql \f0 \sa180 \li0 \fi0 \b \fs32 Import component\par} 29 | {\pard \ql \f0 \sa0 \li360 \fi-360 \bullet \tx360\tab Allows non-technical staff to upload already validated JSON files, and sync the target database accordingly.\par} 30 | {\pard \ql \f0 \sa0 \li360 \fi-360 \bullet \tx360\tab Assumes that input data is relatively safe, as it\u8217's produced via the export component\par} 31 | {\pard \ql \f0 \sa0 \li360 \fi-360 \bullet \tx360\tab Will be optimized to perform quickly; typical runs will be completed within 60 seconds\par} 32 | {\pard \ql \f0 \sa0 \li360 \fi-360 \bullet \tx360\tab Improved validation and error handling\sa180\par} 33 | {\pard \ql \f0 \sa180 \li0 \fi0 \b \fs32 Benefits of our solution\par} 34 | {\pard \ql \f0 \sa180 \li0 \fi0 Here\u8217's some more {\b lorem ipsum}\u160?for you all. Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra.\par} 35 | {\pard \ql \f0 \sa180 \li0 \fi0 Nunc at odio massa. In hac habitasse platea dictumst. Donec posuere erat ac ultrices suscipit. Quisque id velit nulla. Ut in urna orci. Sed at euismod neque. Sed velit velit, fringilla vitae eleifend eget, auctor id mi.\par} 36 | {\pard \ql \f0 \sa180 \li0 \fi0 Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra.\par} 37 | {\pard \ql \f0 \sa180 \li0 \fi0 Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra.\par} 38 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 39 | {\pard \ql \f0 \sa180 \li0 \fi0 \b \fs36 Schedule and Budget\par} 40 | {\pard \ql \f0 \sa180 \li0 \fi0 The work described in this mandate will take 300 hours to complete, to be performed within 6-8 weeks of the signing of this agreement.\par} 41 | {\pard \ql \f0 \sa180 \li0 \fi0 \b \fs32 Table\par} 42 | {\pard \ql \f0 \sa180 \li0 \fi0 Here is a schedule of the work:\par} 43 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 44 | {\pard \ql \f0 \sa180 \li0 \fi0 {\field{\*\fldinst{HYPERLINK "#"}}{\fldrslt{\ul 45 | 46 | }}} 47 | {\field{\*\fldinst{HYPERLINK "#"}}{\fldrslt{\ul 48 | 49 | }}} 50 | \par} 51 | { 52 | \trowd \trgaph120 53 | \cellx4320\cellx8640 54 | \trkeep\intbl 55 | { 56 | {\intbl {\pard \ql \f0 \sa180 \li0 \fi0 gdocs-export:author\par} 57 | \cell} 58 | {\intbl {\pard \ql \f0 \sa180 \li0 \fi0 Alex\par} 59 | \cell} 60 | } 61 | \intbl\row} 62 | { 63 | \trowd \trgaph120 64 | \cellx4320\cellx8640 65 | \trkeep\intbl 66 | { 67 | {\intbl {\pard \ql \f0 \sa180 \li0 \fi0 1\par} 68 | \cell} 69 | {\intbl {\pard \ql \f0 \sa180 \li0 \fi0 150\par} 70 | \cell} 71 | } 72 | \intbl\row} 73 | { 74 | \trowd \trgaph120 75 | \cellx4320\cellx8640 76 | \trkeep\intbl 77 | { 78 | {\intbl {\pard \ql \f0 \sa180 \li0 \fi0 2\par} 79 | \cell} 80 | {\intbl {\pard \ql \f0 \sa180 \li0 \fi0 150\par} 81 | \cell} 82 | } 83 | \intbl\row} 84 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 85 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 86 | {\pard \ql \f0 \sa180 \li0 \fi0 The following diagram should be on a separate page, to test page break:\par} 87 | {\pard \ql \f0 \sa180 \li0 \fi0 \b \fs32 Diagram showing how work is split\par} 88 | {\pard \ql \f0 \sa180 \li0 \fi0 Every document needs a diagram!\par} 89 | {\pard \ql \f0 \sa180 \li0 \fi0 {\cf1 [image: SHbO80B3zYiLwN09wcE1jxTyLWaBsr7llKXblVz0EwCQsT2EsGznwPa4YwxgN3BwyMSEZhyazyYEoA5TNEBswNe6FJ2ZZsw9_E5R3YnzdCYFfcxjoR-rnHg.jpg]\cf0}\par} 90 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 91 | {\pard \ql \f0 \sa180 \li0 \fi0 \b \fs36 Terms and Conditions\par} 92 | {\pard \ql \f0 \sa180 \li0 \fi0 \b \fs32 Staffing\par} 93 | {\pard \ql \f0 \sa180 \li0 \fi0 Appropriate staff will be used for the execution of this project.\par} 94 | {\pard \ql \f0 \sa180 \li0 \fi0 \b \fs32 Relation to Other Agreements\par} 95 | {\pard \ql \f0 \sa180 \li0 \fi0 This agreement is governed by the same terms and conditions as some other documents.\par} 96 | {\pard \ql \f0 \sa180 \li0 \fi0 \b \fs32 Language of Contract\par} 97 | {\pard \ql \f0 \sa180 \li0 \fi0 Les parties ont convenu de r\u233?diger cette entente en anglais. Parties have decided that this agreement be formulated in English.\par} 98 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 99 | {\pard \ql \f0 \sa180 \li0 \fi0 \b \fs36 Agreement\par} 100 | {\pard \ql \f0 \sa180 \li0 \fi0 This document is a contract, and contracts must be signed.\par} 101 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 102 | {\pard \ql \f0 \sa180 \li0 \fi0 Signed on behalf of {\b Client LLC}:\par} 103 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 104 | {\pard \ql \f0 \sa180 \li0 \fi0 \u160?\par} 105 | {\pard \ql \f0 \sa180 \li0 \fi0 \u160?\par} 106 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 107 | {\pard \ql \f0 \sa180 \li0 \fi0 Name and title\par} 108 | {\pard \ql \f0 \sa180 \li0 \fi0 \u160?\par} 109 | {\pard \ql \f0 \sa180 \li0 \fi0 \u160?\par} 110 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 111 | {\pard \ql \f0 \sa180 \li0 \fi0 Signature and date\par} 112 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 113 | {\pard \ql \f0 \sa180 \li0 \fi0 Signed by Adam Smith, President of {\b Vendor Inc.:}\par} 114 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 115 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 116 | {\pard \ql \f0 \sa180 \li0 \fi0 \par} 117 | {\pard \ql \f0 \sa180 \li0 \fi0 Signature and date\par} 118 | } 119 | -------------------------------------------------------------------------------- /build/example/example.tex: -------------------------------------------------------------------------------- 1 | % Docs in http://www.tex.ac.uk/ctan/macros/latex/contrib/memoir/memman.pdf 2 | 3 | \input{metadata.tex} 4 | 5 | \documentclass[letterpaper,oldfontcommands,11pt,extrafontsizes,onecolumn,oneside,openboth,final,titlepage]{memoir} 6 | \usepackage[T1]{fontenc} 7 | \usepackage{lmodern} 8 | \usepackage{amssymb,amsmath} 9 | \usepackage{ifxetex,ifluatex} 10 | \usepackage{fixltx2e} % provides \textsubscript 11 | 12 | % reduce page margins to a reasonable size 13 | \setlrmarginsandblock{2.5cm}{2.5cm}{*} 14 | \setulmarginsandblock{2.5cm}{2.5cm}{*} 15 | \checkandfixthelayout 16 | 17 | 18 | % use microtype if available 19 | \IfFileExists{microtype.sty}{\usepackage{microtype}}{} 20 | % use upquote if available, for straight quotes in verbatim environments 21 | \IfFileExists{upquote.sty}{\usepackage{upquote}}{} 22 | 23 | \usepackage[utf8]{inputenc} 24 | \usepackage{eurosym} 25 | 26 | % http://tex.stackexchange.com/questions/37868/fancyhdr-and-memoir 27 | \let\footruleskip\undefined 28 | 29 | \usepackage{fancyhdr} 30 | \renewcommand{\headrulewidth}{0pt} 31 | 32 | % Chapter 2. Project Description ==> 2. Project Description 33 | \renewcommand{\chaptermark}[1]{\markboth{\thechapter.\ #1}{}} 34 | 35 | \fancypagestyle{plain}{ % used for pages that start new chapters 36 | \lhead{} 37 | \chead{} 38 | \rhead{} 39 | \cfoot{} 40 | \rfoot{\thepage/\thelastpage} 41 | } 42 | 43 | \fancypagestyle{gdocs}{ 44 | \lhead{} 45 | \chead{} 46 | \rhead{\itshape{\nouppercase{\leftmark}}} 47 | \cfoot{} 48 | \rfoot{\thepage/\thelastpage} 49 | } 50 | 51 | \pagestyle{gdocs} 52 | 53 | \chapterstyle{komalike} % also consider article, tandh 54 | 55 | 56 | % used for printing google docs templates 57 | \usepackage{longtable} 58 | % used for printing google docs images 59 | \usepackage{graphicx} 60 | % We will generate all images so they have a width \maxwidth. This means 61 | % that they will get their normal width if they fit onto the page, but 62 | % are scaled down if they would overflow the margins. 63 | \makeatletter 64 | \def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth 65 | \else\Gin@nat@width\fi} 66 | \makeatother 67 | \let\Oldincludegraphics\includegraphics 68 | \renewcommand{\includegraphics}[1]{\Oldincludegraphics[width=\maxwidth]{#1}} 69 | 70 | % used for google docs links 71 | \usepackage[unicode=true]{hyperref} 72 | \hypersetup{breaklinks=true, 73 | bookmarks=true, 74 | pdfauthor=, %TODO: fix the author 75 | pdftitle=\mytitle, 76 | colorlinks=true, 77 | urlcolor=blue, 78 | linkcolor=magenta, 79 | pdfborder={0 0 0}} 80 | 81 | % Uncommnet to make embedded links print out HREF in footnotes, good for printing 82 | % \renewcommand{\href}[2]{#2\footnote{\url{#1}}} 83 | 84 | \setlength{\parindent}{0pt} 85 | \setlength{\parskip}{7pt plus 2pt minus 1pt} 86 | \setlength{\emergencystretch}{3em} % prevent overfull lines 87 | 88 | % Limits depth of automatic numbering of headings. 89 | % 0 => Only H1 (chapters), 1 => Up to H2 (sections), 2 => Up to H3 (subsections) 90 | \setcounter{secnumdepth}{2} 91 | 92 | % memoir specific variable that lowers the title by 200pt 93 | \setlength{\droptitle}{200pt} 94 | 95 | % customize size and alignment of title and subtitle 96 | \pretitle{\begin{flushleft}} 97 | \title{\bfseries \HUGE \mytitle \\ \vskip .5em \LARGE \mysubtitle} 98 | \posttitle{\par\vskip1em{\normalfont\normalsize\scshape \par\vfill}\end{flushleft}\vskip 0.5em} 99 | 100 | % TODO generate the author somehow 101 | % \author{} 102 | 103 | % Suppress printing todays date. 104 | % TODO: currently looks bad if not blank; make it look good and replace with todays date 105 | \date{} 106 | 107 | \begin{document} 108 | 109 | \input{header.tex} 110 | 111 | \setcounter{tocdepth}{1} 112 | \begin{KeepFromToc} 113 | \hypersetup{linkcolor=black} 114 | \tableofcontents 115 | \end{KeepFromToc} 116 | \pagebreak 117 | 118 | \input{main.tex} 119 | 120 | \end{document} 121 | -------------------------------------------------------------------------------- /build/example/example.toc: -------------------------------------------------------------------------------- 1 | \contentsline {chapter}{\chapternumberline {1}Background}{3}{chapter.1} 2 | \contentsline {chapter}{\chapternumberline {2}Proposed Solution}{4}{chapter.2} 3 | \contentsline {section}{\numberline {2.1}Export component}{4}{section.2.1} 4 | \contentsline {section}{\numberline {2.2}Import component}{4}{section.2.2} 5 | \contentsline {section}{\numberline {2.3}Benefits of our solution}{4}{section.2.3} 6 | \contentsline {chapter}{\chapternumberline {3}Schedule and Budget}{6}{chapter.3} 7 | \contentsline {section}{\numberline {3.1}Table}{6}{section.3.1} 8 | \contentsline {section}{\numberline {3.2}Diagram showing how work is split}{7}{section.3.2} 9 | \contentsline {chapter}{\chapternumberline {4}Terms and Conditions}{8}{chapter.4} 10 | \contentsline {section}{\numberline {4.1}Staffing}{8}{section.4.1} 11 | \contentsline {section}{\numberline {4.2}Relation to Other Agreements}{8}{section.4.2} 12 | \contentsline {section}{\numberline {4.3}Language of Contract}{8}{section.4.3} 13 | \contentsline {chapter}{\chapternumberline {5}Agreement}{9}{chapter.5} 14 | -------------------------------------------------------------------------------- /build/example/header.tex: -------------------------------------------------------------------------------- 1 | \begin{titlingpage} 2 | \maketitle 3 | \vfill 4 | \Oldincludegraphics[width=3in]{./UNlogo.png} 5 | \end{titlingpage} 6 | 7 | \pagebreak 8 | 9 | \textbf{Prepared by:} 10 | 11 | Vendor Inc.,\\49 Seven square\\Montreal, QC H0H 0H0\\phone: (514) 12 | 345-6789\\fax: (514) 345-6789\\http://vendor.com 13 | 14 | \textbf{} 15 | 16 | \textbf{Prepared for:} 17 | 18 | Client LLC,\\37 Prime Lane\\Montreal, QC H0H 0H0\\phone: (514) 19 | 345-6789\\fax: (514) 345-6789\\http://supplier.com 20 | 21 | \vfill 22 | 23 | CONFIDENTIAL: The contents of this document are confidential and are 24 | intended exclusively for the designated recipients. The contents of this page 25 | is defined in a file called sample-header.tex. Other legal mumbo jumbo\ldots{}. 26 | 27 | \pagebreak 28 | -------------------------------------------------------------------------------- /build/example/in.html: -------------------------------------------------------------------------------- 1 | Test doc for gd-pandoc

Sample Statement of Work

Every document needs a subtitle...


Background

Every project needs a background. Here’s some lorem ipsum about the project.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sit amet arcu elit. Donec at massa et leo auctor fringilla vitae id erat. Nam feugiat velit vitae ornare tristique. Morbi porttitor, orci vel dapibus malesuada, magna libero ultrices massa, nec interdum diam nisi elementum ipsum. Vestibulum elit lorem, pretium id semper ac, aliquam ut dui. Etiam at magna non nibh viverra tristique. Maecenas consectetur vulputate tellus, ac rutrum felis molestie ut. Aenean tincidunt congue cursus. Maecenas in egestas magna, nec pellentesque justo. Phasellus ante quam, faucibus sit amet tellus et, fringilla convallis eros.

Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra.

Nunc at odio massa. In hac habitasse platea dictumst. Donec posuere erat ac ultrices suscipit. Quisque id velit nulla. Ut in urna orci. Sed at euismod neque. Sed velit velit, fringilla vitae eleifend eget, auctor id mi.


Proposed Solution

This wouldn’t be a proposal unless it proposed something. We will re-architect the system into a series of robust robust components:

  1. export, a script to export the data from access, validate it, refactor it into a logical format, then output it in plain-text format.
  2. import, a simple, robust module to handle file uploads of the cleaned data, and efficiently import this data. It will do sufficient error handling to allow full automation.

As we perform the refactoring, we will rewriting the code to ensure it’s documented, easy to test, and easy to update as future needs are identified.

Export component

Import component

Benefits of our solution

Here’s some more lorem ipsum for you all. Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra.

Nunc at odio massa. In hac habitasse platea dictumst. Donec posuere erat ac ultrices suscipit. Quisque id velit nulla. Ut in urna orci. Sed at euismod neque. Sed velit velit, fringilla vitae eleifend eget, auctor id mi.

Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra.

Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra.


Schedule and Budget

The work described in this mandate will take 300 hours to complete, to be performed within 6-8 weeks of the signing of this agreement.

Table

Here is a schedule of the work:

gdocs-export:author

Alex

1

150

2

150

The following diagram should be on a separate page, to test page break:


Diagram showing how work is split

Every document needs a diagram!


Terms and Conditions

Staffing

Appropriate staff will be used for the execution of this project.

Relation to Other Agreements

This agreement is governed by the same terms and conditions as some other documents.

Language of Contract

Les parties ont convenu de rédiger cette entente en anglais. Parties have decided that this agreement be formulated in English.


Agreement

This document is a contract, and contracts must be signed.

Signed on behalf of Client LLC:

 

 


Name and title

 

 


Signature and date

Signed by Adam Smith, President of Vendor Inc.:


Signature and date

2 | -------------------------------------------------------------------------------- /build/example/main.tex: -------------------------------------------------------------------------------- 1 | \pagebreak 2 | 3 | \hyperdef{}{}{\chapter{Background}} 4 | 5 | Every project needs a background. Here's some lorem ipsum about the project. 6 | 7 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sit amet arcu elit. Donec at massa et leo auctor fringilla vitae id erat. Nam feugiat velit vitae ornare tristique. Morbi porttitor, orci vel dapibus malesuada, magna libero ultrices massa, nec interdum diam nisi elementum ipsum. Vestibulum elit lorem, pretium id semper ac, aliquam ut dui. Etiam at magna non nibh viverra tristique. Maecenas consectetur vulputate tellus, ac rutrum felis molestie ut. Aenean tincidunt congue cursus. Maecenas in egestas magna, nec pellentesque justo. Phasellus ante quam, faucibus sit amet tellus et, fringilla convallis eros. 8 | 9 | Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra. 10 | 11 | Nunc at odio massa. In hac habitasse platea dictumst. Donec posuere erat ac ultrices suscipit. Quisque id velit nulla. Ut in urna orci. Sed at euismod neque. Sed velit velit, fringilla vitae eleifend eget, auctor id mi. 12 | 13 | \pagebreak 14 | 15 | \hyperdef{}{}{\chapter{Proposed Solution}} 16 | 17 | This wouldn't be a proposal unless it proposed something. We will re-architect the system into a series of robust robust components: 18 | 19 | \begin{enumerate} 20 | \itemsep1pt\parskip0pt\parsep0pt 21 | \item 22 | export, a script to export the data from access, validate it, refactor it into a logical format, then output it in plain-text format. 23 | \item 24 | import, a simple, robust module to handle file uploads of the cleaned data, and efficiently import this data. It will do sufficient error handling to allow full automation. 25 | \end{enumerate} 26 | 27 | As we perform the refactoring, we will rewriting the code to ensure it's documented, easy to test, and easy to update as future needs are identified. 28 | 29 | \hyperdef{}{}{\section{Export component}} 30 | 31 | \begin{itemize} 32 | \itemsep1pt\parskip0pt\parsep0pt 33 | \item 34 | This will be a standalone script responsible for extracting data from source database, and cleaning it up as appropriate. 35 | \item 36 | The output format is human readable JSON files, with a logical, easy to understand structure 37 | \item 38 | Easy to verify output through manual inspection of output 39 | \item 40 | Include comprehensive automated tests to ensure output is correct 41 | \end{itemize} 42 | 43 | \hyperdef{}{}{\section{Import component}} 44 | 45 | \begin{itemize} 46 | \itemsep1pt\parskip0pt\parsep0pt 47 | \item 48 | Allows non-technical staff to upload already validated JSON files, and sync the target database accordingly. 49 | \item 50 | Assumes that input data is relatively safe, as it's produced via the export component 51 | \item 52 | Will be optimized to perform quickly; typical runs will be completed within 60 seconds 53 | \item 54 | Improved validation and error handling 55 | \end{itemize} 56 | 57 | \hyperdef{}{}{\section{Benefits of our solution}} 58 | 59 | Here's some more \textbf{lorem ipsum}~for you all. Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra. 60 | 61 | Nunc at odio massa. In hac habitasse platea dictumst. Donec posuere erat ac ultrices suscipit. Quisque id velit nulla. Ut in urna orci. Sed at euismod neque. Sed velit velit, fringilla vitae eleifend eget, auctor id mi. 62 | 63 | Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra. 64 | 65 | Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra. 66 | 67 | \pagebreak 68 | 69 | \hyperdef{}{}{\chapter{Schedule and Budget}} 70 | 71 | The work described in this mandate will take 300 hours to complete, to be performed within 6-8 weeks of the signing of this agreement. 72 | 73 | \hyperdef{}{}{\section{Table}} 74 | 75 | Here is a schedule of the work: 76 | 77 | \hyperref[]{}\hyperref[]{} 78 | 79 | \begin{longtable}[c]{@{}ll@{}} 80 | \toprule\addlinespace 81 | \begin{minipage}[t]{0.47\columnwidth}\raggedright 82 | gdocs-export:author 83 | \end{minipage} & \begin{minipage}[t]{0.47\columnwidth}\raggedright 84 | Alex 85 | \end{minipage} 86 | \\\addlinespace 87 | \begin{minipage}[t]{0.47\columnwidth}\raggedright 88 | 1 89 | \end{minipage} & \begin{minipage}[t]{0.47\columnwidth}\raggedright 90 | 150 91 | \end{minipage} 92 | \\\addlinespace 93 | \begin{minipage}[t]{0.47\columnwidth}\raggedright 94 | 2 95 | \end{minipage} & \begin{minipage}[t]{0.47\columnwidth}\raggedright 96 | 150 97 | \end{minipage} 98 | \\\addlinespace 99 | \bottomrule 100 | \end{longtable} 101 | 102 | The following diagram should be on a separate page, to test page break: 103 | 104 | \pagebreak 105 | 106 | \hyperdef{}{}{\section{Diagram showing how work is split}} 107 | 108 | Every document needs a diagram! 109 | 110 | \includegraphics{SHbO80B3zYiLwN09wcE1jxTyLWaBsr7llKXblVz0EwCQsT2EsGznwPa4YwxgN3BwyMSEZhyazyYEoA5TNEBswNe6FJ2ZZsw9_E5R3YnzdCYFfcxjoR-rnHg.jpg} 111 | 112 | \pagebreak 113 | 114 | \hyperdef{}{}{\chapter{Terms and Conditions}} 115 | 116 | \hyperdef{}{}{\section{Staffing}} 117 | 118 | Appropriate staff will be used for the execution of this project. 119 | 120 | \hyperdef{}{}{\section{Relation to Other Agreements}} 121 | 122 | This agreement is governed by the same terms and conditions as some other documents. 123 | 124 | \hyperdef{}{}{\section{Language of Contract}} 125 | 126 | Les parties ont convenu de rédiger cette entente en anglais. Parties have decided that this agreement be formulated in English. 127 | 128 | \pagebreak 129 | 130 | \hyperdef{}{}{\chapter{Agreement}} 131 | 132 | This document is a contract, and contracts must be signed. 133 | 134 | Signed on behalf of \textbf{Client LLC}: 135 | 136 | ~ 137 | 138 | ~ 139 | 140 | \hrulefill 141 | 142 | Name and title 143 | 144 | ~ 145 | 146 | ~ 147 | 148 | \hrulefill 149 | 150 | Signature and date 151 | 152 | Signed by Adam Smith, President of \textbf{Vendor Inc.:} 153 | 154 | \hrulefill 155 | 156 | Signature and date 157 | -------------------------------------------------------------------------------- /build/example/metadata.tex: -------------------------------------------------------------------------------- 1 | \newcommand{\mytitle}{Sample Statement of Work} 2 | \newcommand{\mysubtitle}{Every document needs a subtitle...} 3 | \newcommand{\mybody}{\pagebreak 4 | 5 | \hyperdef{}{}{\section{Background}} 6 | 7 | Every project needs a background. Here's some lorem ipsum about the project. 8 | 9 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sit amet arcu elit. Donec at massa et leo auctor fringilla vitae id erat. Nam feugiat velit vitae ornare tristique. Morbi porttitor, orci vel dapibus malesuada, magna libero ultrices massa, nec interdum diam nisi elementum ipsum. Vestibulum elit lorem, pretium id semper ac, aliquam ut dui. Etiam at magna non nibh viverra tristique. Maecenas consectetur vulputate tellus, ac rutrum felis molestie ut. Aenean tincidunt congue cursus. Maecenas in egestas magna, nec pellentesque justo. Phasellus ante quam, faucibus sit amet tellus et, fringilla convallis eros. 10 | 11 | Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra. 12 | 13 | Nunc at odio massa. In hac habitasse platea dictumst. Donec posuere erat ac ultrices suscipit. Quisque id velit nulla. Ut in urna orci. Sed at euismod neque. Sed velit velit, fringilla vitae eleifend eget, auctor id mi. 14 | 15 | \pagebreak 16 | 17 | \hyperdef{}{}{\section{Proposed Solution}} 18 | 19 | This wouldn't be a proposal unless it proposed something. We will re-architect the system into a series of robust robust components: 20 | 21 | \begin{enumerate} 22 | \itemsep1pt\parskip0pt\parsep0pt 23 | \item 24 | export, a script to export the data from access, validate it, refactor it into a logical format, then output it in plain-text format. 25 | \item 26 | import, a simple, robust module to handle file uploads of the cleaned data, and efficiently import this data. It will do sufficient error handling to allow full automation. 27 | \end{enumerate} 28 | 29 | As we perform the refactoring, we will rewriting the code to ensure it's documented, easy to test, and easy to update as future needs are identified. 30 | 31 | \hyperdef{}{}{\subsection{Export component}} 32 | 33 | \begin{itemize} 34 | \itemsep1pt\parskip0pt\parsep0pt 35 | \item 36 | This will be a standalone script responsible for extracting data from source database, and cleaning it up as appropriate. 37 | \item 38 | The output format is human readable JSON files, with a logical, easy to understand structure 39 | \item 40 | Easy to verify output through manual inspection of output 41 | \item 42 | Include comprehensive automated tests to ensure output is correct 43 | \end{itemize} 44 | 45 | \hyperdef{}{}{\subsection{Import component}} 46 | 47 | \begin{itemize} 48 | \itemsep1pt\parskip0pt\parsep0pt 49 | \item 50 | Allows non-technical staff to upload already validated JSON files, and sync the target database accordingly. 51 | \item 52 | Assumes that input data is relatively safe, as it's produced via the export component 53 | \item 54 | Will be optimized to perform quickly; typical runs will be completed within 60 seconds 55 | \item 56 | Improved validation and error handling 57 | \end{itemize} 58 | 59 | \hyperdef{}{}{\subsection{Benefits of our solution}} 60 | 61 | Here's some more \textbf{lorem ipsum}~for you all. Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra. 62 | 63 | Nunc at odio massa. In hac habitasse platea dictumst. Donec posuere erat ac ultrices suscipit. Quisque id velit nulla. Ut in urna orci. Sed at euismod neque. Sed velit velit, fringilla vitae eleifend eget, auctor id mi. 64 | 65 | Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra. 66 | 67 | Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra. 68 | 69 | \pagebreak 70 | 71 | \hyperdef{}{}{\section{Schedule and Budget}} 72 | 73 | The work described in this mandate will take 300 hours to complete, to be performed within 6-8 weeks of the signing of this agreement. 74 | 75 | \hyperdef{}{}{\subsection{Table}} 76 | 77 | Here is a schedule of the work: 78 | 79 | \hyperref[]{}\hyperref[]{} 80 | 81 | \begin{longtable}[c]{@{}ll@{}} 82 | \toprule\addlinespace 83 | \begin{minipage}[t]{0.47\columnwidth}\raggedright 84 | gdocs-export:author 85 | \end{minipage} & \begin{minipage}[t]{0.47\columnwidth}\raggedright 86 | Alex 87 | \end{minipage} 88 | \\\addlinespace 89 | \begin{minipage}[t]{0.47\columnwidth}\raggedright 90 | 1 91 | \end{minipage} & \begin{minipage}[t]{0.47\columnwidth}\raggedright 92 | 150 93 | \end{minipage} 94 | \\\addlinespace 95 | \begin{minipage}[t]{0.47\columnwidth}\raggedright 96 | 2 97 | \end{minipage} & \begin{minipage}[t]{0.47\columnwidth}\raggedright 98 | 150 99 | \end{minipage} 100 | \\\addlinespace 101 | \bottomrule 102 | \end{longtable} 103 | 104 | The following diagram should be on a separate page, to test page break: 105 | 106 | \pagebreak 107 | 108 | \hyperdef{}{}{\subsection{Diagram showing how work is split}} 109 | 110 | Every document needs a diagram! 111 | 112 | \includegraphics{SHbO80B3zYiLwN09wcE1jxTyLWaBsr7llKXblVz0EwCQsT2EsGznwPa4YwxgN3BwyMSEZhyazyYEoA5TNEBswNe6FJ2ZZsw9_E5R3YnzdCYFfcxjoR-rnHg.jpg} 113 | 114 | \pagebreak 115 | 116 | \hyperdef{}{}{\section{Terms and Conditions}} 117 | 118 | \hyperdef{}{}{\subsection{Staffing}} 119 | 120 | Appropriate staff will be used for the execution of this project. 121 | 122 | \hyperdef{}{}{\subsection{Relation to Other Agreements}} 123 | 124 | This agreement is governed by the same terms and conditions as some other documents. 125 | 126 | \hyperdef{}{}{\subsection{Language of Contract}} 127 | 128 | Les parties ont convenu de rédiger cette entente en anglais. Parties have decided that this agreement be formulated in English. 129 | 130 | \pagebreak 131 | 132 | \hyperdef{}{}{\section{Agreement}} 133 | 134 | This document is a contract, and contracts must be signed. 135 | 136 | Signed on behalf of \textbf{Client LLC}: 137 | 138 | ~ 139 | 140 | ~ 141 | 142 | \hrulefill 143 | 144 | Name and title 145 | 146 | ~ 147 | 148 | ~ 149 | 150 | \hrulefill 151 | 152 | Signature and date 153 | 154 | Signed by Adam Smith, President of \textbf{Vendor Inc.:} 155 | 156 | \hrulefill 157 | 158 | Signature and date} 159 | 160 | 161 | % TODO: support table of key-value metadata in google doc 162 | -------------------------------------------------------------------------------- /build/example/pre.json: -------------------------------------------------------------------------------- 1 | [{"unMeta":{"title":{"t":"MetaInlines","c":[{"t":"Str","c":"Test"},{"t":"Space","c":[]},{"t":"Str","c":"doc"},{"t":"Space","c":[]},{"t":"Str","c":"for"},{"t":"Space","c":[]},{"t":"Str","c":"gd"},{"t":"Str","c":"-"},{"t":"Str","c":"pandoc"}]}}},[{"t":"Header","c":[1,["",["ew-pandoc-title"],[]],[{"t":"Str","c":"Sample"},{"t":"Space","c":[]},{"t":"Str","c":"Statement"},{"t":"Space","c":[]},{"t":"Str","c":"of"},{"t":"Space","c":[]},{"t":"Str","c":"Work"}]]},{"t":"Header","c":[1,["",["ew-pandoc-subtitle"],[]],[{"t":"Str","c":"Every"},{"t":"Space","c":[]},{"t":"Str","c":"document"},{"t":"Space","c":[]},{"t":"Str","c":"needs"},{"t":"Space","c":[]},{"t":"Str","c":"a"},{"t":"Space","c":[]},{"t":"Str","c":"subtitle"},{"t":"Str","c":"."},{"t":"Str","c":"."},{"t":"Str","c":"."}]]},{"t":"Para","c":[]},{"t":"Para","c":[]},{"t":"Para","c":[]},{"t":"Para","c":[]},{"t":"Header","c":[1,["",["ew-pandoc-pagebreak"],[]],[]]},{"t":"Para","c":[]},{"t":"Header","c":[1,["",["c1"],[]],[{"t":"Str","c":"Background"}]]},{"t":"Para","c":[{"t":"Str","c":"Every"},{"t":"Space","c":[]},{"t":"Str","c":"project"},{"t":"Space","c":[]},{"t":"Str","c":"needs"},{"t":"Space","c":[]},{"t":"Str","c":"a"},{"t":"Space","c":[]},{"t":"Str","c":"background"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Here"},{"t":"Str","c":"’"},{"t":"Str","c":"s"},{"t":"Space","c":[]},{"t":"Str","c":"some"},{"t":"Space","c":[]},{"t":"Str","c":"lorem"},{"t":"Space","c":[]},{"t":"Str","c":"ipsum"},{"t":"Space","c":[]},{"t":"Str","c":"about"},{"t":"Space","c":[]},{"t":"Str","c":"the"},{"t":"Space","c":[]},{"t":"Str","c":"project"},{"t":"Str","c":"."}]},{"t":"Para","c":[{"t":"Str","c":"Lorem"},{"t":"Space","c":[]},{"t":"Str","c":"ipsum"},{"t":"Space","c":[]},{"t":"Str","c":"dolor"},{"t":"Space","c":[]},{"t":"Str","c":"sit"},{"t":"Space","c":[]},{"t":"Str","c":"amet,"},{"t":"Space","c":[]},{"t":"Str","c":"consectetur"},{"t":"Space","c":[]},{"t":"Str","c":"adipiscing"},{"t":"Space","c":[]},{"t":"Str","c":"elit"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Nulla"},{"t":"Space","c":[]},{"t":"Str","c":"sit"},{"t":"Space","c":[]},{"t":"Str","c":"amet"},{"t":"Space","c":[]},{"t":"Str","c":"arcu"},{"t":"Space","c":[]},{"t":"Str","c":"elit"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Donec"},{"t":"Space","c":[]},{"t":"Str","c":"at"},{"t":"Space","c":[]},{"t":"Str","c":"massa"},{"t":"Space","c":[]},{"t":"Str","c":"et"},{"t":"Space","c":[]},{"t":"Str","c":"leo"},{"t":"Space","c":[]},{"t":"Str","c":"auctor"},{"t":"Space","c":[]},{"t":"Str","c":"fringilla"},{"t":"Space","c":[]},{"t":"Str","c":"vitae"},{"t":"Space","c":[]},{"t":"Str","c":"id"},{"t":"Space","c":[]},{"t":"Str","c":"erat"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Nam"},{"t":"Space","c":[]},{"t":"Str","c":"feugiat"},{"t":"Space","c":[]},{"t":"Str","c":"velit"},{"t":"Space","c":[]},{"t":"Str","c":"vitae"},{"t":"Space","c":[]},{"t":"Str","c":"ornare"},{"t":"Space","c":[]},{"t":"Str","c":"tristique"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Morbi"},{"t":"Space","c":[]},{"t":"Str","c":"porttitor,"},{"t":"Space","c":[]},{"t":"Str","c":"orci"},{"t":"Space","c":[]},{"t":"Str","c":"vel"},{"t":"Space","c":[]},{"t":"Str","c":"dapibus"},{"t":"Space","c":[]},{"t":"Str","c":"malesuada,"},{"t":"Space","c":[]},{"t":"Str","c":"magna"},{"t":"Space","c":[]},{"t":"Str","c":"libero"},{"t":"Space","c":[]},{"t":"Str","c":"ultrices"},{"t":"Space","c":[]},{"t":"Str","c":"massa,"},{"t":"Space","c":[]},{"t":"Str","c":"nec"},{"t":"Space","c":[]},{"t":"Str","c":"interdum"},{"t":"Space","c":[]},{"t":"Str","c":"diam"},{"t":"Space","c":[]},{"t":"Str","c":"nisi"},{"t":"Space","c":[]},{"t":"Str","c":"elementum"},{"t":"Space","c":[]},{"t":"Str","c":"ipsum"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Vestibulum"},{"t":"Space","c":[]},{"t":"Str","c":"elit"},{"t":"Space","c":[]},{"t":"Str","c":"lorem,"},{"t":"Space","c":[]},{"t":"Str","c":"pretium"},{"t":"Space","c":[]},{"t":"Str","c":"id"},{"t":"Space","c":[]},{"t":"Str","c":"semper"},{"t":"Space","c":[]},{"t":"Str","c":"ac,"},{"t":"Space","c":[]},{"t":"Str","c":"aliquam"},{"t":"Space","c":[]},{"t":"Str","c":"ut"},{"t":"Space","c":[]},{"t":"Str","c":"dui"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Etiam"},{"t":"Space","c":[]},{"t":"Str","c":"at"},{"t":"Space","c":[]},{"t":"Str","c":"magna"},{"t":"Space","c":[]},{"t":"Str","c":"non"},{"t":"Space","c":[]},{"t":"Str","c":"nibh"},{"t":"Space","c":[]},{"t":"Str","c":"viverra"},{"t":"Space","c":[]},{"t":"Str","c":"tristique"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Maecenas"},{"t":"Space","c":[]},{"t":"Str","c":"consectetur"},{"t":"Space","c":[]},{"t":"Str","c":"vulputate"},{"t":"Space","c":[]},{"t":"Str","c":"tellus,"},{"t":"Space","c":[]},{"t":"Str","c":"ac"},{"t":"Space","c":[]},{"t":"Str","c":"rutrum"},{"t":"Space","c":[]},{"t":"Str","c":"felis"},{"t":"Space","c":[]},{"t":"Str","c":"molestie"},{"t":"Space","c":[]},{"t":"Str","c":"ut"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Aenean"},{"t":"Space","c":[]},{"t":"Str","c":"tincidunt"},{"t":"Space","c":[]},{"t":"Str","c":"congue"},{"t":"Space","c":[]},{"t":"Str","c":"cursus"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Maecenas"},{"t":"Space","c":[]},{"t":"Str","c":"in"},{"t":"Space","c":[]},{"t":"Str","c":"egestas"},{"t":"Space","c":[]},{"t":"Str","c":"magna,"},{"t":"Space","c":[]},{"t":"Str","c":"nec"},{"t":"Space","c":[]},{"t":"Str","c":"pellentesque"},{"t":"Space","c":[]},{"t":"Str","c":"justo"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Phasellus"},{"t":"Space","c":[]},{"t":"Str","c":"ante"},{"t":"Space","c":[]},{"t":"Str","c":"quam,"},{"t":"Space","c":[]},{"t":"Str","c":"faucibus"},{"t":"Space","c":[]},{"t":"Str","c":"sit"},{"t":"Space","c":[]},{"t":"Str","c":"amet"},{"t":"Space","c":[]},{"t":"Str","c":"tellus"},{"t":"Space","c":[]},{"t":"Str","c":"et,"},{"t":"Space","c":[]},{"t":"Str","c":"fringilla"},{"t":"Space","c":[]},{"t":"Str","c":"convallis"},{"t":"Space","c":[]},{"t":"Str","c":"eros"},{"t":"Str","c":"."}]},{"t":"Para","c":[{"t":"Str","c":"Nullam"},{"t":"Space","c":[]},{"t":"Str","c":"vitae"},{"t":"Space","c":[]},{"t":"Str","c":"imperdiet"},{"t":"Space","c":[]},{"t":"Str","c":"enim"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Ut"},{"t":"Space","c":[]},{"t":"Str","c":"id"},{"t":"Space","c":[]},{"t":"Str","c":"ultrices"},{"t":"Space","c":[]},{"t":"Str","c":"leo"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"In"},{"t":"Space","c":[]},{"t":"Str","c":"dictum"},{"t":"Space","c":[]},{"t":"Str","c":"bibendum"},{"t":"Space","c":[]},{"t":"Str","c":"vestibulum"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Donec"},{"t":"Space","c":[]},{"t":"Str","c":"lobortis"},{"t":"Space","c":[]},{"t":"Str","c":"condimentum"},{"t":"Space","c":[]},{"t":"Str","c":"mauris"},{"t":"Space","c":[]},{"t":"Str","c":"ac"},{"t":"Space","c":[]},{"t":"Str","c":"interdum"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Duis"},{"t":"Space","c":[]},{"t":"Str","c":"bibendum"},{"t":"Space","c":[]},{"t":"Str","c":"molestie"},{"t":"Space","c":[]},{"t":"Str","c":"eleifend"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Nunc"},{"t":"Space","c":[]},{"t":"Str","c":"non"},{"t":"Space","c":[]},{"t":"Str","c":"neque"},{"t":"Space","c":[]},{"t":"Str","c":"a"},{"t":"Space","c":[]},{"t":"Str","c":"neque"},{"t":"Space","c":[]},{"t":"Str","c":"ornare"},{"t":"Space","c":[]},{"t":"Str","c":"euismod"},{"t":"Space","c":[]},{"t":"Str","c":"non"},{"t":"Space","c":[]},{"t":"Str","c":"ut"},{"t":"Space","c":[]},{"t":"Str","c":"sapien"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Morbi"},{"t":"Space","c":[]},{"t":"Str","c":"sed"},{"t":"Space","c":[]},{"t":"Str","c":"diam"},{"t":"Space","c":[]},{"t":"Str","c":"quis"},{"t":"Space","c":[]},{"t":"Str","c":"felis"},{"t":"Space","c":[]},{"t":"Str","c":"commodo"},{"t":"Space","c":[]},{"t":"Str","c":"vehicula"},{"t":"Space","c":[]},{"t":"Str","c":"ac"},{"t":"Space","c":[]},{"t":"Str","c":"et"},{"t":"Space","c":[]},{"t":"Str","c":"augue"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Maecenas"},{"t":"Space","c":[]},{"t":"Str","c":"pretium"},{"t":"Space","c":[]},{"t":"Str","c":"justo"},{"t":"Space","c":[]},{"t":"Str","c":"sit"},{"t":"Space","c":[]},{"t":"Str","c":"amet"},{"t":"Space","c":[]},{"t":"Str","c":"imperdiet"},{"t":"Space","c":[]},{"t":"Str","c":"pharetra"},{"t":"Str","c":"."}]},{"t":"Para","c":[{"t":"Str","c":"Nunc"},{"t":"Space","c":[]},{"t":"Str","c":"at"},{"t":"Space","c":[]},{"t":"Str","c":"odio"},{"t":"Space","c":[]},{"t":"Str","c":"massa"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"In"},{"t":"Space","c":[]},{"t":"Str","c":"hac"},{"t":"Space","c":[]},{"t":"Str","c":"habitasse"},{"t":"Space","c":[]},{"t":"Str","c":"platea"},{"t":"Space","c":[]},{"t":"Str","c":"dictumst"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Donec"},{"t":"Space","c":[]},{"t":"Str","c":"posuere"},{"t":"Space","c":[]},{"t":"Str","c":"erat"},{"t":"Space","c":[]},{"t":"Str","c":"ac"},{"t":"Space","c":[]},{"t":"Str","c":"ultrices"},{"t":"Space","c":[]},{"t":"Str","c":"suscipit"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Quisque"},{"t":"Space","c":[]},{"t":"Str","c":"id"},{"t":"Space","c":[]},{"t":"Str","c":"velit"},{"t":"Space","c":[]},{"t":"Str","c":"nulla"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Ut"},{"t":"Space","c":[]},{"t":"Str","c":"in"},{"t":"Space","c":[]},{"t":"Str","c":"urna"},{"t":"Space","c":[]},{"t":"Str","c":"orci"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Sed"},{"t":"Space","c":[]},{"t":"Str","c":"at"},{"t":"Space","c":[]},{"t":"Str","c":"euismod"},{"t":"Space","c":[]},{"t":"Str","c":"neque"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Sed"},{"t":"Space","c":[]},{"t":"Str","c":"velit"},{"t":"Space","c":[]},{"t":"Str","c":"velit,"},{"t":"Space","c":[]},{"t":"Str","c":"fringilla"},{"t":"Space","c":[]},{"t":"Str","c":"vitae"},{"t":"Space","c":[]},{"t":"Str","c":"eleifend"},{"t":"Space","c":[]},{"t":"Str","c":"eget,"},{"t":"Space","c":[]},{"t":"Str","c":"auctor"},{"t":"Space","c":[]},{"t":"Str","c":"id"},{"t":"Space","c":[]},{"t":"Str","c":"mi"},{"t":"Str","c":"."}]},{"t":"Header","c":[1,["",["ew-pandoc-pagebreak"],[]],[]]},{"t":"Para","c":[]},{"t":"Header","c":[1,["",["c1"],[]],[{"t":"Str","c":"Proposed"},{"t":"Space","c":[]},{"t":"Str","c":"Solution"}]]},{"t":"Para","c":[{"t":"Str","c":"This"},{"t":"Space","c":[]},{"t":"Str","c":"wouldn"},{"t":"Str","c":"’"},{"t":"Str","c":"t"},{"t":"Space","c":[]},{"t":"Str","c":"be"},{"t":"Space","c":[]},{"t":"Str","c":"a"},{"t":"Space","c":[]},{"t":"Str","c":"proposal"},{"t":"Space","c":[]},{"t":"Str","c":"unless"},{"t":"Space","c":[]},{"t":"Str","c":"it"},{"t":"Space","c":[]},{"t":"Str","c":"proposed"},{"t":"Space","c":[]},{"t":"Str","c":"something"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"We"},{"t":"Space","c":[]},{"t":"Str","c":"will"},{"t":"Space","c":[]},{"t":"Str","c":"re"},{"t":"Str","c":"-"},{"t":"Str","c":"architect"},{"t":"Space","c":[]},{"t":"Str","c":"the"},{"t":"Space","c":[]},{"t":"Str","c":"system"},{"t":"Space","c":[]},{"t":"Str","c":"into"},{"t":"Space","c":[]},{"t":"Str","c":"a"},{"t":"Space","c":[]},{"t":"Str","c":"series"},{"t":"Space","c":[]},{"t":"Str","c":"of"},{"t":"Space","c":[]},{"t":"Str","c":"robust"},{"t":"Space","c":[]},{"t":"Str","c":"robust"},{"t":"Space","c":[]},{"t":"Str","c":"components:"}]},{"t":"OrderedList","c":[[1,{"t":"DefaultStyle","c":[]},{"t":"DefaultDelim","c":[]}],[[{"t":"Plain","c":[{"t":"Str","c":"export,"},{"t":"Space","c":[]},{"t":"Str","c":"a"},{"t":"Space","c":[]},{"t":"Str","c":"script"},{"t":"Space","c":[]},{"t":"Str","c":"to"},{"t":"Space","c":[]},{"t":"Str","c":"export"},{"t":"Space","c":[]},{"t":"Str","c":"the"},{"t":"Space","c":[]},{"t":"Str","c":"data"},{"t":"Space","c":[]},{"t":"Str","c":"from"},{"t":"Space","c":[]},{"t":"Str","c":"access,"},{"t":"Space","c":[]},{"t":"Str","c":"validate"},{"t":"Space","c":[]},{"t":"Str","c":"it,"},{"t":"Space","c":[]},{"t":"Str","c":"refactor"},{"t":"Space","c":[]},{"t":"Str","c":"it"},{"t":"Space","c":[]},{"t":"Str","c":"into"},{"t":"Space","c":[]},{"t":"Str","c":"a"},{"t":"Space","c":[]},{"t":"Str","c":"logical"},{"t":"Space","c":[]},{"t":"Str","c":"format,"},{"t":"Space","c":[]},{"t":"Str","c":"then"},{"t":"Space","c":[]},{"t":"Str","c":"output"},{"t":"Space","c":[]},{"t":"Str","c":"it"},{"t":"Space","c":[]},{"t":"Str","c":"in"},{"t":"Space","c":[]},{"t":"Str","c":"plain"},{"t":"Str","c":"-"},{"t":"Str","c":"text"},{"t":"Space","c":[]},{"t":"Str","c":"format"},{"t":"Str","c":"."}]}],[{"t":"Plain","c":[{"t":"Str","c":"import,"},{"t":"Space","c":[]},{"t":"Str","c":"a"},{"t":"Space","c":[]},{"t":"Str","c":"simple,"},{"t":"Space","c":[]},{"t":"Str","c":"robust"},{"t":"Space","c":[]},{"t":"Str","c":"module"},{"t":"Space","c":[]},{"t":"Str","c":"to"},{"t":"Space","c":[]},{"t":"Str","c":"handle"},{"t":"Space","c":[]},{"t":"Str","c":"file"},{"t":"Space","c":[]},{"t":"Str","c":"uploads"},{"t":"Space","c":[]},{"t":"Str","c":"of"},{"t":"Space","c":[]},{"t":"Str","c":"the"},{"t":"Space","c":[]},{"t":"Str","c":"cleaned"},{"t":"Space","c":[]},{"t":"Str","c":"data,"},{"t":"Space","c":[]},{"t":"Str","c":"and"},{"t":"Space","c":[]},{"t":"Str","c":"efficiently"},{"t":"Space","c":[]},{"t":"Str","c":"import"},{"t":"Space","c":[]},{"t":"Str","c":"this"},{"t":"Space","c":[]},{"t":"Str","c":"data"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"It"},{"t":"Space","c":[]},{"t":"Str","c":"will"},{"t":"Space","c":[]},{"t":"Str","c":"do"},{"t":"Space","c":[]},{"t":"Str","c":"sufficient"},{"t":"Space","c":[]},{"t":"Str","c":"error"},{"t":"Space","c":[]},{"t":"Str","c":"handling"},{"t":"Space","c":[]},{"t":"Str","c":"to"},{"t":"Space","c":[]},{"t":"Str","c":"allow"},{"t":"Space","c":[]},{"t":"Str","c":"full"},{"t":"Space","c":[]},{"t":"Str","c":"automation"},{"t":"Str","c":"."}]}]]]},{"t":"Para","c":[{"t":"Str","c":"As"},{"t":"Space","c":[]},{"t":"Str","c":"we"},{"t":"Space","c":[]},{"t":"Str","c":"perform"},{"t":"Space","c":[]},{"t":"Str","c":"the"},{"t":"Space","c":[]},{"t":"Str","c":"refactoring,"},{"t":"Space","c":[]},{"t":"Str","c":"we"},{"t":"Space","c":[]},{"t":"Str","c":"will"},{"t":"Space","c":[]},{"t":"Str","c":"rewriting"},{"t":"Space","c":[]},{"t":"Str","c":"the"},{"t":"Space","c":[]},{"t":"Str","c":"code"},{"t":"Space","c":[]},{"t":"Str","c":"to"},{"t":"Space","c":[]},{"t":"Str","c":"ensure"},{"t":"Space","c":[]},{"t":"Str","c":"it"},{"t":"Str","c":"’"},{"t":"Str","c":"s"},{"t":"Space","c":[]},{"t":"Str","c":"documented,"},{"t":"Space","c":[]},{"t":"Str","c":"easy"},{"t":"Space","c":[]},{"t":"Str","c":"to"},{"t":"Space","c":[]},{"t":"Str","c":"test,"},{"t":"Space","c":[]},{"t":"Str","c":"and"},{"t":"Space","c":[]},{"t":"Str","c":"easy"},{"t":"Space","c":[]},{"t":"Str","c":"to"},{"t":"Space","c":[]},{"t":"Str","c":"update"},{"t":"Space","c":[]},{"t":"Str","c":"as"},{"t":"Space","c":[]},{"t":"Str","c":"future"},{"t":"Space","c":[]},{"t":"Str","c":"needs"},{"t":"Space","c":[]},{"t":"Str","c":"are"},{"t":"Space","c":[]},{"t":"Str","c":"identified"},{"t":"Str","c":"."}]},{"t":"Header","c":[2,["",["c1"],[]],[{"t":"Str","c":"Export"},{"t":"Space","c":[]},{"t":"Str","c":"component"}]]},{"t":"BulletList","c":[[{"t":"Plain","c":[{"t":"Str","c":"This"},{"t":"Space","c":[]},{"t":"Str","c":"will"},{"t":"Space","c":[]},{"t":"Str","c":"be"},{"t":"Space","c":[]},{"t":"Str","c":"a"},{"t":"Space","c":[]},{"t":"Str","c":"standalone"},{"t":"Space","c":[]},{"t":"Str","c":"script"},{"t":"Space","c":[]},{"t":"Str","c":"responsible"},{"t":"Space","c":[]},{"t":"Str","c":"for"},{"t":"Space","c":[]},{"t":"Str","c":"extracting"},{"t":"Space","c":[]},{"t":"Str","c":"data"},{"t":"Space","c":[]},{"t":"Str","c":"from"},{"t":"Space","c":[]},{"t":"Str","c":"source"},{"t":"Space","c":[]},{"t":"Str","c":"database,"},{"t":"Space","c":[]},{"t":"Str","c":"and"},{"t":"Space","c":[]},{"t":"Str","c":"cleaning"},{"t":"Space","c":[]},{"t":"Str","c":"it"},{"t":"Space","c":[]},{"t":"Str","c":"up"},{"t":"Space","c":[]},{"t":"Str","c":"as"},{"t":"Space","c":[]},{"t":"Str","c":"appropriate"},{"t":"Str","c":"."}]}],[{"t":"Plain","c":[{"t":"Str","c":"The"},{"t":"Space","c":[]},{"t":"Str","c":"output"},{"t":"Space","c":[]},{"t":"Str","c":"format"},{"t":"Space","c":[]},{"t":"Str","c":"is"},{"t":"Space","c":[]},{"t":"Str","c":"human"},{"t":"Space","c":[]},{"t":"Str","c":"readable"},{"t":"Space","c":[]},{"t":"Str","c":"JSON"},{"t":"Space","c":[]},{"t":"Str","c":"files,"},{"t":"Space","c":[]},{"t":"Str","c":"with"},{"t":"Space","c":[]},{"t":"Str","c":"a"},{"t":"Space","c":[]},{"t":"Str","c":"logical,"},{"t":"Space","c":[]},{"t":"Str","c":"easy"},{"t":"Space","c":[]},{"t":"Str","c":"to"},{"t":"Space","c":[]},{"t":"Str","c":"understand"},{"t":"Space","c":[]},{"t":"Str","c":"structure"}]}],[{"t":"Plain","c":[{"t":"Str","c":"Easy"},{"t":"Space","c":[]},{"t":"Str","c":"to"},{"t":"Space","c":[]},{"t":"Str","c":"verify"},{"t":"Space","c":[]},{"t":"Str","c":"output"},{"t":"Space","c":[]},{"t":"Str","c":"through"},{"t":"Space","c":[]},{"t":"Str","c":"manual"},{"t":"Space","c":[]},{"t":"Str","c":"inspection"},{"t":"Space","c":[]},{"t":"Str","c":"of"},{"t":"Space","c":[]},{"t":"Str","c":"output"}]}],[{"t":"Plain","c":[{"t":"Str","c":"Include"},{"t":"Space","c":[]},{"t":"Str","c":"comprehensive"},{"t":"Space","c":[]},{"t":"Str","c":"automated"},{"t":"Space","c":[]},{"t":"Str","c":"tests"},{"t":"Space","c":[]},{"t":"Str","c":"to"},{"t":"Space","c":[]},{"t":"Str","c":"ensure"},{"t":"Space","c":[]},{"t":"Str","c":"output"},{"t":"Space","c":[]},{"t":"Str","c":"is"},{"t":"Space","c":[]},{"t":"Str","c":"correct"}]}]]},{"t":"Header","c":[2,["",["c1"],[]],[{"t":"Str","c":"Import"},{"t":"Space","c":[]},{"t":"Str","c":"component"}]]},{"t":"BulletList","c":[[{"t":"Plain","c":[{"t":"Str","c":"Allows"},{"t":"Space","c":[]},{"t":"Str","c":"non"},{"t":"Str","c":"-"},{"t":"Str","c":"technical"},{"t":"Space","c":[]},{"t":"Str","c":"staff"},{"t":"Space","c":[]},{"t":"Str","c":"to"},{"t":"Space","c":[]},{"t":"Str","c":"upload"},{"t":"Space","c":[]},{"t":"Str","c":"already"},{"t":"Space","c":[]},{"t":"Str","c":"validated"},{"t":"Space","c":[]},{"t":"Str","c":"JSON"},{"t":"Space","c":[]},{"t":"Str","c":"files,"},{"t":"Space","c":[]},{"t":"Str","c":"and"},{"t":"Space","c":[]},{"t":"Str","c":"sync"},{"t":"Space","c":[]},{"t":"Str","c":"the"},{"t":"Space","c":[]},{"t":"Str","c":"target"},{"t":"Space","c":[]},{"t":"Str","c":"database"},{"t":"Space","c":[]},{"t":"Str","c":"accordingly"},{"t":"Str","c":"."}]}],[{"t":"Plain","c":[{"t":"Str","c":"Assumes"},{"t":"Space","c":[]},{"t":"Str","c":"that"},{"t":"Space","c":[]},{"t":"Str","c":"input"},{"t":"Space","c":[]},{"t":"Str","c":"data"},{"t":"Space","c":[]},{"t":"Str","c":"is"},{"t":"Space","c":[]},{"t":"Str","c":"relatively"},{"t":"Space","c":[]},{"t":"Str","c":"safe,"},{"t":"Space","c":[]},{"t":"Str","c":"as"},{"t":"Space","c":[]},{"t":"Str","c":"it"},{"t":"Str","c":"’"},{"t":"Str","c":"s"},{"t":"Space","c":[]},{"t":"Str","c":"produced"},{"t":"Space","c":[]},{"t":"Str","c":"via"},{"t":"Space","c":[]},{"t":"Str","c":"the"},{"t":"Space","c":[]},{"t":"Str","c":"export"},{"t":"Space","c":[]},{"t":"Str","c":"component"}]}],[{"t":"Plain","c":[{"t":"Str","c":"Will"},{"t":"Space","c":[]},{"t":"Str","c":"be"},{"t":"Space","c":[]},{"t":"Str","c":"optimized"},{"t":"Space","c":[]},{"t":"Str","c":"to"},{"t":"Space","c":[]},{"t":"Str","c":"perform"},{"t":"Space","c":[]},{"t":"Str","c":"quickly;"},{"t":"Space","c":[]},{"t":"Str","c":"typical"},{"t":"Space","c":[]},{"t":"Str","c":"runs"},{"t":"Space","c":[]},{"t":"Str","c":"will"},{"t":"Space","c":[]},{"t":"Str","c":"be"},{"t":"Space","c":[]},{"t":"Str","c":"completed"},{"t":"Space","c":[]},{"t":"Str","c":"within"},{"t":"Space","c":[]},{"t":"Str","c":"60"},{"t":"Space","c":[]},{"t":"Str","c":"seconds"}]}],[{"t":"Plain","c":[{"t":"Str","c":"Improved"},{"t":"Space","c":[]},{"t":"Str","c":"validation"},{"t":"Space","c":[]},{"t":"Str","c":"and"},{"t":"Space","c":[]},{"t":"Str","c":"error"},{"t":"Space","c":[]},{"t":"Str","c":"handling"}]}]]},{"t":"Header","c":[2,["",["c1"],[]],[{"t":"Str","c":"Benefits"},{"t":"Space","c":[]},{"t":"Str","c":"of"},{"t":"Space","c":[]},{"t":"Str","c":"our"},{"t":"Space","c":[]},{"t":"Str","c":"solution"}]]},{"t":"Para","c":[{"t":"Str","c":"Here"},{"t":"Str","c":"’"},{"t":"Str","c":"s"},{"t":"Space","c":[]},{"t":"Str","c":"some"},{"t":"Space","c":[]},{"t":"Str","c":"more"},{"t":"Space","c":[]},{"t":"Strong","c":[{"t":"Str","c":"lorem"},{"t":"Space","c":[]},{"t":"Str","c":"ipsum"}]},{"t":"Str","c":" for"},{"t":"Space","c":[]},{"t":"Str","c":"you"},{"t":"Space","c":[]},{"t":"Str","c":"all"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Nullam"},{"t":"Space","c":[]},{"t":"Str","c":"vitae"},{"t":"Space","c":[]},{"t":"Str","c":"imperdiet"},{"t":"Space","c":[]},{"t":"Str","c":"enim"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Ut"},{"t":"Space","c":[]},{"t":"Str","c":"id"},{"t":"Space","c":[]},{"t":"Str","c":"ultrices"},{"t":"Space","c":[]},{"t":"Str","c":"leo"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"In"},{"t":"Space","c":[]},{"t":"Str","c":"dictum"},{"t":"Space","c":[]},{"t":"Str","c":"bibendum"},{"t":"Space","c":[]},{"t":"Str","c":"vestibulum"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Donec"},{"t":"Space","c":[]},{"t":"Str","c":"lobortis"},{"t":"Space","c":[]},{"t":"Str","c":"condimentum"},{"t":"Space","c":[]},{"t":"Str","c":"mauris"},{"t":"Space","c":[]},{"t":"Str","c":"ac"},{"t":"Space","c":[]},{"t":"Str","c":"interdum"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Duis"},{"t":"Space","c":[]},{"t":"Str","c":"bibendum"},{"t":"Space","c":[]},{"t":"Str","c":"molestie"},{"t":"Space","c":[]},{"t":"Str","c":"eleifend"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Nunc"},{"t":"Space","c":[]},{"t":"Str","c":"non"},{"t":"Space","c":[]},{"t":"Str","c":"neque"},{"t":"Space","c":[]},{"t":"Str","c":"a"},{"t":"Space","c":[]},{"t":"Str","c":"neque"},{"t":"Space","c":[]},{"t":"Str","c":"ornare"},{"t":"Space","c":[]},{"t":"Str","c":"euismod"},{"t":"Space","c":[]},{"t":"Str","c":"non"},{"t":"Space","c":[]},{"t":"Str","c":"ut"},{"t":"Space","c":[]},{"t":"Str","c":"sapien"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Morbi"},{"t":"Space","c":[]},{"t":"Str","c":"sed"},{"t":"Space","c":[]},{"t":"Str","c":"diam"},{"t":"Space","c":[]},{"t":"Str","c":"quis"},{"t":"Space","c":[]},{"t":"Str","c":"felis"},{"t":"Space","c":[]},{"t":"Str","c":"commodo"},{"t":"Space","c":[]},{"t":"Str","c":"vehicula"},{"t":"Space","c":[]},{"t":"Str","c":"ac"},{"t":"Space","c":[]},{"t":"Str","c":"et"},{"t":"Space","c":[]},{"t":"Str","c":"augue"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Maecenas"},{"t":"Space","c":[]},{"t":"Str","c":"pretium"},{"t":"Space","c":[]},{"t":"Str","c":"justo"},{"t":"Space","c":[]},{"t":"Str","c":"sit"},{"t":"Space","c":[]},{"t":"Str","c":"amet"},{"t":"Space","c":[]},{"t":"Str","c":"imperdiet"},{"t":"Space","c":[]},{"t":"Str","c":"pharetra"},{"t":"Str","c":"."}]},{"t":"Para","c":[{"t":"Str","c":"Nunc"},{"t":"Space","c":[]},{"t":"Str","c":"at"},{"t":"Space","c":[]},{"t":"Str","c":"odio"},{"t":"Space","c":[]},{"t":"Str","c":"massa"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"In"},{"t":"Space","c":[]},{"t":"Str","c":"hac"},{"t":"Space","c":[]},{"t":"Str","c":"habitasse"},{"t":"Space","c":[]},{"t":"Str","c":"platea"},{"t":"Space","c":[]},{"t":"Str","c":"dictumst"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Donec"},{"t":"Space","c":[]},{"t":"Str","c":"posuere"},{"t":"Space","c":[]},{"t":"Str","c":"erat"},{"t":"Space","c":[]},{"t":"Str","c":"ac"},{"t":"Space","c":[]},{"t":"Str","c":"ultrices"},{"t":"Space","c":[]},{"t":"Str","c":"suscipit"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Quisque"},{"t":"Space","c":[]},{"t":"Str","c":"id"},{"t":"Space","c":[]},{"t":"Str","c":"velit"},{"t":"Space","c":[]},{"t":"Str","c":"nulla"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Ut"},{"t":"Space","c":[]},{"t":"Str","c":"in"},{"t":"Space","c":[]},{"t":"Str","c":"urna"},{"t":"Space","c":[]},{"t":"Str","c":"orci"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Sed"},{"t":"Space","c":[]},{"t":"Str","c":"at"},{"t":"Space","c":[]},{"t":"Str","c":"euismod"},{"t":"Space","c":[]},{"t":"Str","c":"neque"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Sed"},{"t":"Space","c":[]},{"t":"Str","c":"velit"},{"t":"Space","c":[]},{"t":"Str","c":"velit,"},{"t":"Space","c":[]},{"t":"Str","c":"fringilla"},{"t":"Space","c":[]},{"t":"Str","c":"vitae"},{"t":"Space","c":[]},{"t":"Str","c":"eleifend"},{"t":"Space","c":[]},{"t":"Str","c":"eget,"},{"t":"Space","c":[]},{"t":"Str","c":"auctor"},{"t":"Space","c":[]},{"t":"Str","c":"id"},{"t":"Space","c":[]},{"t":"Str","c":"mi"},{"t":"Str","c":"."}]},{"t":"Para","c":[{"t":"Str","c":"Nullam"},{"t":"Space","c":[]},{"t":"Str","c":"vitae"},{"t":"Space","c":[]},{"t":"Str","c":"imperdiet"},{"t":"Space","c":[]},{"t":"Str","c":"enim"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Ut"},{"t":"Space","c":[]},{"t":"Str","c":"id"},{"t":"Space","c":[]},{"t":"Str","c":"ultrices"},{"t":"Space","c":[]},{"t":"Str","c":"leo"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"In"},{"t":"Space","c":[]},{"t":"Str","c":"dictum"},{"t":"Space","c":[]},{"t":"Str","c":"bibendum"},{"t":"Space","c":[]},{"t":"Str","c":"vestibulum"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Donec"},{"t":"Space","c":[]},{"t":"Str","c":"lobortis"},{"t":"Space","c":[]},{"t":"Str","c":"condimentum"},{"t":"Space","c":[]},{"t":"Str","c":"mauris"},{"t":"Space","c":[]},{"t":"Str","c":"ac"},{"t":"Space","c":[]},{"t":"Str","c":"interdum"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Duis"},{"t":"Space","c":[]},{"t":"Str","c":"bibendum"},{"t":"Space","c":[]},{"t":"Str","c":"molestie"},{"t":"Space","c":[]},{"t":"Str","c":"eleifend"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Nunc"},{"t":"Space","c":[]},{"t":"Str","c":"non"},{"t":"Space","c":[]},{"t":"Str","c":"neque"},{"t":"Space","c":[]},{"t":"Str","c":"a"},{"t":"Space","c":[]},{"t":"Str","c":"neque"},{"t":"Space","c":[]},{"t":"Str","c":"ornare"},{"t":"Space","c":[]},{"t":"Str","c":"euismod"},{"t":"Space","c":[]},{"t":"Str","c":"non"},{"t":"Space","c":[]},{"t":"Str","c":"ut"},{"t":"Space","c":[]},{"t":"Str","c":"sapien"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Morbi"},{"t":"Space","c":[]},{"t":"Str","c":"sed"},{"t":"Space","c":[]},{"t":"Str","c":"diam"},{"t":"Space","c":[]},{"t":"Str","c":"quis"},{"t":"Space","c":[]},{"t":"Str","c":"felis"},{"t":"Space","c":[]},{"t":"Str","c":"commodo"},{"t":"Space","c":[]},{"t":"Str","c":"vehicula"},{"t":"Space","c":[]},{"t":"Str","c":"ac"},{"t":"Space","c":[]},{"t":"Str","c":"et"},{"t":"Space","c":[]},{"t":"Str","c":"augue"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Maecenas"},{"t":"Space","c":[]},{"t":"Str","c":"pretium"},{"t":"Space","c":[]},{"t":"Str","c":"justo"},{"t":"Space","c":[]},{"t":"Str","c":"sit"},{"t":"Space","c":[]},{"t":"Str","c":"amet"},{"t":"Space","c":[]},{"t":"Str","c":"imperdiet"},{"t":"Space","c":[]},{"t":"Str","c":"pharetra"},{"t":"Str","c":"."}]},{"t":"Para","c":[{"t":"Str","c":"Nullam"},{"t":"Space","c":[]},{"t":"Str","c":"vitae"},{"t":"Space","c":[]},{"t":"Str","c":"imperdiet"},{"t":"Space","c":[]},{"t":"Str","c":"enim"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Ut"},{"t":"Space","c":[]},{"t":"Str","c":"id"},{"t":"Space","c":[]},{"t":"Str","c":"ultrices"},{"t":"Space","c":[]},{"t":"Str","c":"leo"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"In"},{"t":"Space","c":[]},{"t":"Str","c":"dictum"},{"t":"Space","c":[]},{"t":"Str","c":"bibendum"},{"t":"Space","c":[]},{"t":"Str","c":"vestibulum"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Donec"},{"t":"Space","c":[]},{"t":"Str","c":"lobortis"},{"t":"Space","c":[]},{"t":"Str","c":"condimentum"},{"t":"Space","c":[]},{"t":"Str","c":"mauris"},{"t":"Space","c":[]},{"t":"Str","c":"ac"},{"t":"Space","c":[]},{"t":"Str","c":"interdum"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Duis"},{"t":"Space","c":[]},{"t":"Str","c":"bibendum"},{"t":"Space","c":[]},{"t":"Str","c":"molestie"},{"t":"Space","c":[]},{"t":"Str","c":"eleifend"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Nunc"},{"t":"Space","c":[]},{"t":"Str","c":"non"},{"t":"Space","c":[]},{"t":"Str","c":"neque"},{"t":"Space","c":[]},{"t":"Str","c":"a"},{"t":"Space","c":[]},{"t":"Str","c":"neque"},{"t":"Space","c":[]},{"t":"Str","c":"ornare"},{"t":"Space","c":[]},{"t":"Str","c":"euismod"},{"t":"Space","c":[]},{"t":"Str","c":"non"},{"t":"Space","c":[]},{"t":"Str","c":"ut"},{"t":"Space","c":[]},{"t":"Str","c":"sapien"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Morbi"},{"t":"Space","c":[]},{"t":"Str","c":"sed"},{"t":"Space","c":[]},{"t":"Str","c":"diam"},{"t":"Space","c":[]},{"t":"Str","c":"quis"},{"t":"Space","c":[]},{"t":"Str","c":"felis"},{"t":"Space","c":[]},{"t":"Str","c":"commodo"},{"t":"Space","c":[]},{"t":"Str","c":"vehicula"},{"t":"Space","c":[]},{"t":"Str","c":"ac"},{"t":"Space","c":[]},{"t":"Str","c":"et"},{"t":"Space","c":[]},{"t":"Str","c":"augue"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Maecenas"},{"t":"Space","c":[]},{"t":"Str","c":"pretium"},{"t":"Space","c":[]},{"t":"Str","c":"justo"},{"t":"Space","c":[]},{"t":"Str","c":"sit"},{"t":"Space","c":[]},{"t":"Str","c":"amet"},{"t":"Space","c":[]},{"t":"Str","c":"imperdiet"},{"t":"Space","c":[]},{"t":"Str","c":"pharetra"},{"t":"Str","c":"."}]},{"t":"Header","c":[1,["",["ew-pandoc-pagebreak"],[]],[]]},{"t":"Para","c":[]},{"t":"Header","c":[1,["",["c1"],[]],[{"t":"Str","c":"Schedule"},{"t":"Space","c":[]},{"t":"Str","c":"and"},{"t":"Space","c":[]},{"t":"Str","c":"Budget"}]]},{"t":"Para","c":[{"t":"Str","c":"The"},{"t":"Space","c":[]},{"t":"Str","c":"work"},{"t":"Space","c":[]},{"t":"Str","c":"described"},{"t":"Space","c":[]},{"t":"Str","c":"in"},{"t":"Space","c":[]},{"t":"Str","c":"this"},{"t":"Space","c":[]},{"t":"Str","c":"mandate"},{"t":"Space","c":[]},{"t":"Str","c":"will"},{"t":"Space","c":[]},{"t":"Str","c":"take"},{"t":"Space","c":[]},{"t":"Str","c":"300"},{"t":"Space","c":[]},{"t":"Str","c":"hours"},{"t":"Space","c":[]},{"t":"Str","c":"to"},{"t":"Space","c":[]},{"t":"Str","c":"complete,"},{"t":"Space","c":[]},{"t":"Str","c":"to"},{"t":"Space","c":[]},{"t":"Str","c":"be"},{"t":"Space","c":[]},{"t":"Str","c":"performed"},{"t":"Space","c":[]},{"t":"Str","c":"within"},{"t":"Space","c":[]},{"t":"Str","c":"6"},{"t":"Str","c":"-"},{"t":"Str","c":"8"},{"t":"Space","c":[]},{"t":"Str","c":"weeks"},{"t":"Space","c":[]},{"t":"Str","c":"of"},{"t":"Space","c":[]},{"t":"Str","c":"the"},{"t":"Space","c":[]},{"t":"Str","c":"signing"},{"t":"Space","c":[]},{"t":"Str","c":"of"},{"t":"Space","c":[]},{"t":"Str","c":"this"},{"t":"Space","c":[]},{"t":"Str","c":"agreement"},{"t":"Str","c":"."}]},{"t":"Header","c":[2,["",["c1"],[]],[{"t":"Str","c":"Table"}]]},{"t":"Para","c":[{"t":"Str","c":"Here"},{"t":"Space","c":[]},{"t":"Str","c":"is"},{"t":"Space","c":[]},{"t":"Str","c":"a"},{"t":"Space","c":[]},{"t":"Str","c":"schedule"},{"t":"Space","c":[]},{"t":"Str","c":"of"},{"t":"Space","c":[]},{"t":"Str","c":"the"},{"t":"Space","c":[]},{"t":"Str","c":"work:"}]},{"t":"Para","c":[]},{"t":"Para","c":[{"t":"Link","c":[[],["#",""]]},{"t":"Link","c":[[],["#",""]]}]},{"t":"Table","c":[[],[{"t":"AlignLeft","c":[]},{"t":"AlignLeft","c":[]}],[0.5,0.5],[],[[[{"t":"Para","c":[{"t":"Str","c":"gdocs"},{"t":"Str","c":"-"},{"t":"Str","c":"export:author"}]}],[{"t":"Para","c":[{"t":"Str","c":"Alex"}]}]],[[{"t":"Para","c":[{"t":"Str","c":"1"}]}],[{"t":"Para","c":[{"t":"Str","c":"150"}]}]],[[{"t":"Para","c":[{"t":"Str","c":"2"}]}],[{"t":"Para","c":[{"t":"Str","c":"150"}]}]]]]},{"t":"Para","c":[]},{"t":"Para","c":[{"t":"Str","c":"The"},{"t":"Space","c":[]},{"t":"Str","c":"following"},{"t":"Space","c":[]},{"t":"Str","c":"diagram"},{"t":"Space","c":[]},{"t":"Str","c":"should"},{"t":"Space","c":[]},{"t":"Str","c":"be"},{"t":"Space","c":[]},{"t":"Str","c":"on"},{"t":"Space","c":[]},{"t":"Str","c":"a"},{"t":"Space","c":[]},{"t":"Str","c":"separate"},{"t":"Space","c":[]},{"t":"Str","c":"page,"},{"t":"Space","c":[]},{"t":"Str","c":"to"},{"t":"Space","c":[]},{"t":"Str","c":"test"},{"t":"Space","c":[]},{"t":"Str","c":"page"},{"t":"Space","c":[]},{"t":"Str","c":"break:"}]},{"t":"Header","c":[1,["",["ew-pandoc-pagebreak"],[]],[]]},{"t":"Header","c":[2,["",["c1"],[]],[{"t":"Str","c":"Diagram"},{"t":"Space","c":[]},{"t":"Str","c":"showing"},{"t":"Space","c":[]},{"t":"Str","c":"how"},{"t":"Space","c":[]},{"t":"Str","c":"work"},{"t":"Space","c":[]},{"t":"Str","c":"is"},{"t":"Space","c":[]},{"t":"Str","c":"split"}]]},{"t":"Para","c":[{"t":"Str","c":"Every"},{"t":"Space","c":[]},{"t":"Str","c":"document"},{"t":"Space","c":[]},{"t":"Str","c":"needs"},{"t":"Space","c":[]},{"t":"Str","c":"a"},{"t":"Space","c":[]},{"t":"Str","c":"diagram!"}]},{"t":"Para","c":[{"t":"Image","c":[[],["SHbO80B3zYiLwN09wcE1jxTyLWaBsr7llKXblVz0EwCQsT2EsGznwPa4YwxgN3BwyMSEZhyazyYEoA5TNEBswNe6FJ2ZZsw9_E5R3YnzdCYFfcxjoR-rnHg.jpg",""]]}]},{"t":"Header","c":[1,["",["ew-pandoc-pagebreak"],[]],[]]},{"t":"Para","c":[]},{"t":"Header","c":[1,["",["c1"],[]],[{"t":"Str","c":"Terms"},{"t":"Space","c":[]},{"t":"Str","c":"and"},{"t":"Space","c":[]},{"t":"Str","c":"Conditions"}]]},{"t":"Header","c":[2,["",["c1"],[]],[{"t":"Str","c":"Staffing"}]]},{"t":"Para","c":[{"t":"Str","c":"Appropriate"},{"t":"Space","c":[]},{"t":"Str","c":"staff"},{"t":"Space","c":[]},{"t":"Str","c":"will"},{"t":"Space","c":[]},{"t":"Str","c":"be"},{"t":"Space","c":[]},{"t":"Str","c":"used"},{"t":"Space","c":[]},{"t":"Str","c":"for"},{"t":"Space","c":[]},{"t":"Str","c":"the"},{"t":"Space","c":[]},{"t":"Str","c":"execution"},{"t":"Space","c":[]},{"t":"Str","c":"of"},{"t":"Space","c":[]},{"t":"Str","c":"this"},{"t":"Space","c":[]},{"t":"Str","c":"project"},{"t":"Str","c":"."}]},{"t":"Header","c":[2,["",["c1"],[]],[{"t":"Str","c":"Relation"},{"t":"Space","c":[]},{"t":"Str","c":"to"},{"t":"Space","c":[]},{"t":"Str","c":"Other"},{"t":"Space","c":[]},{"t":"Str","c":"Agreements"}]]},{"t":"Para","c":[{"t":"Str","c":"This"},{"t":"Space","c":[]},{"t":"Str","c":"agreement"},{"t":"Space","c":[]},{"t":"Str","c":"is"},{"t":"Space","c":[]},{"t":"Str","c":"governed"},{"t":"Space","c":[]},{"t":"Str","c":"by"},{"t":"Space","c":[]},{"t":"Str","c":"the"},{"t":"Space","c":[]},{"t":"Str","c":"same"},{"t":"Space","c":[]},{"t":"Str","c":"terms"},{"t":"Space","c":[]},{"t":"Str","c":"and"},{"t":"Space","c":[]},{"t":"Str","c":"conditions"},{"t":"Space","c":[]},{"t":"Str","c":"as"},{"t":"Space","c":[]},{"t":"Str","c":"some"},{"t":"Space","c":[]},{"t":"Str","c":"other"},{"t":"Space","c":[]},{"t":"Str","c":"documents"},{"t":"Str","c":"."}]},{"t":"Header","c":[2,["",["c1"],[]],[{"t":"Str","c":"Language"},{"t":"Space","c":[]},{"t":"Str","c":"of"},{"t":"Space","c":[]},{"t":"Str","c":"Contract"}]]},{"t":"Para","c":[{"t":"Str","c":"Les"},{"t":"Space","c":[]},{"t":"Str","c":"parties"},{"t":"Space","c":[]},{"t":"Str","c":"ont"},{"t":"Space","c":[]},{"t":"Str","c":"convenu"},{"t":"Space","c":[]},{"t":"Str","c":"de"},{"t":"Space","c":[]},{"t":"Str","c":"rédiger"},{"t":"Space","c":[]},{"t":"Str","c":"cette"},{"t":"Space","c":[]},{"t":"Str","c":"entente"},{"t":"Space","c":[]},{"t":"Str","c":"en"},{"t":"Space","c":[]},{"t":"Str","c":"anglais"},{"t":"Str","c":"."},{"t":"Space","c":[]},{"t":"Str","c":"Parties"},{"t":"Space","c":[]},{"t":"Str","c":"have"},{"t":"Space","c":[]},{"t":"Str","c":"decided"},{"t":"Space","c":[]},{"t":"Str","c":"that"},{"t":"Space","c":[]},{"t":"Str","c":"this"},{"t":"Space","c":[]},{"t":"Str","c":"agreement"},{"t":"Space","c":[]},{"t":"Str","c":"be"},{"t":"Space","c":[]},{"t":"Str","c":"formulated"},{"t":"Space","c":[]},{"t":"Str","c":"in"},{"t":"Space","c":[]},{"t":"Str","c":"English"},{"t":"Str","c":"."}]},{"t":"Header","c":[1,["",["ew-pandoc-pagebreak"],[]],[]]},{"t":"Para","c":[]},{"t":"Header","c":[1,["",["c1","c11"],[]],[{"t":"Str","c":"Agreement"}]]},{"t":"Para","c":[{"t":"Str","c":"This"},{"t":"Space","c":[]},{"t":"Str","c":"document"},{"t":"Space","c":[]},{"t":"Str","c":"is"},{"t":"Space","c":[]},{"t":"Str","c":"a"},{"t":"Space","c":[]},{"t":"Str","c":"contract,"},{"t":"Space","c":[]},{"t":"Str","c":"and"},{"t":"Space","c":[]},{"t":"Str","c":"contracts"},{"t":"Space","c":[]},{"t":"Str","c":"must"},{"t":"Space","c":[]},{"t":"Str","c":"be"},{"t":"Space","c":[]},{"t":"Str","c":"signed"},{"t":"Str","c":"."}]},{"t":"Para","c":[]},{"t":"Para","c":[{"t":"Str","c":"Signed"},{"t":"Space","c":[]},{"t":"Str","c":"on"},{"t":"Space","c":[]},{"t":"Str","c":"behalf"},{"t":"Space","c":[]},{"t":"Str","c":"of"},{"t":"Space","c":[]},{"t":"Strong","c":[{"t":"Str","c":"Client"},{"t":"Space","c":[]},{"t":"Str","c":"LLC"}]},{"t":"Str","c":":"}]},{"t":"Para","c":[]},{"t":"Para","c":[{"t":"Str","c":" "}]},{"t":"Para","c":[{"t":"Str","c":" "}]},{"t":"HorizontalRule","c":[]},{"t":"Para","c":[]},{"t":"Para","c":[{"t":"Str","c":"Name"},{"t":"Space","c":[]},{"t":"Str","c":"and"},{"t":"Space","c":[]},{"t":"Str","c":"title"}]},{"t":"Para","c":[{"t":"Str","c":" "}]},{"t":"Para","c":[{"t":"Str","c":" "}]},{"t":"HorizontalRule","c":[]},{"t":"Para","c":[]},{"t":"Para","c":[{"t":"Str","c":"Signature"},{"t":"Space","c":[]},{"t":"Str","c":"and"},{"t":"Space","c":[]},{"t":"Str","c":"date"}]},{"t":"Para","c":[]},{"t":"Para","c":[{"t":"Str","c":"Signed"},{"t":"Space","c":[]},{"t":"Str","c":"by"},{"t":"Space","c":[]},{"t":"Str","c":"Adam"},{"t":"Space","c":[]},{"t":"Str","c":"Smith,"},{"t":"Space","c":[]},{"t":"Str","c":"President"},{"t":"Space","c":[]},{"t":"Str","c":"of"},{"t":"Space","c":[]},{"t":"Strong","c":[{"t":"Str","c":"Vendor"},{"t":"Space","c":[]},{"t":"Str","c":"Inc"},{"t":"Str","c":"."},{"t":"Str","c":":"}]}]},{"t":"Para","c":[]},{"t":"Para","c":[]},{"t":"HorizontalRule","c":[]},{"t":"Para","c":[]},{"t":"Para","c":[{"t":"Str","c":"Signature"},{"t":"Space","c":[]},{"t":"Str","c":"and"},{"t":"Space","c":[]},{"t":"Str","c":"date"}]}]] 2 | -------------------------------------------------------------------------------- /build/example/preprocessed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test doc for gd-pandoc 5 | 6 | 7 | 8 | 9 |

Sample Statement of Work

10 |

Every document needs a subtitle...

11 |

12 |

13 |

14 |

15 |

16 |

17 |

18 | Background 19 |

20 |

Every project needs a background. Here’s some lorem ipsum about the project.

21 |

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sit amet arcu elit. Donec at massa et leo auctor fringilla vitae id erat. Nam feugiat velit vitae ornare tristique. Morbi porttitor, orci vel dapibus malesuada, magna libero ultrices massa, nec interdum diam nisi elementum ipsum. Vestibulum elit lorem, pretium id semper ac, aliquam ut dui. Etiam at magna non nibh viverra tristique. Maecenas consectetur vulputate tellus, ac rutrum felis molestie ut. Aenean tincidunt congue cursus. Maecenas in egestas magna, nec pellentesque justo. Phasellus ante quam, faucibus sit amet tellus et, fringilla convallis eros.

22 |

Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra.

23 |

Nunc at odio massa. In hac habitasse platea dictumst. Donec posuere erat ac ultrices suscipit. Quisque id velit nulla. Ut in urna orci. Sed at euismod neque. Sed velit velit, fringilla vitae eleifend eget, auctor id mi.

24 |

25 |

26 |

27 | Proposed Solution 28 |

29 |

This wouldn’t be a proposal unless it proposed something. We will re-architect the system into a series of robust robust components:

30 |
    31 |
  1. export, a script to export the data from access, validate it, refactor it into a logical format, then output it in plain-text format.
  2. 32 |
  3. import, a simple, robust module to handle file uploads of the cleaned data, and efficiently import this data. It will do sufficient error handling to allow full automation.
  4. 33 |
34 |

As we perform the refactoring, we will rewriting the code to ensure it’s documented, easy to test, and easy to update as future needs are identified.

35 |

36 | Export component 37 |

38 | 44 |

45 | Import component 46 |

47 | 53 |

54 | Benefits of our solution 55 |

56 |

Here’s some more lorem ipsum for you all. Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra.

57 |

Nunc at odio massa. In hac habitasse platea dictumst. Donec posuere erat ac ultrices suscipit. Quisque id velit nulla. Ut in urna orci. Sed at euismod neque. Sed velit velit, fringilla vitae eleifend eget, auctor id mi.

58 |

Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra.

59 |

Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra.

60 |

61 |

62 |

63 | Schedule and Budget 64 |

65 |

The work described in this mandate will take 300 hours to complete, to be performed within 6-8 weeks of the signing of this agreement.

66 |

67 | Table 68 |

69 |

Here is a schedule of the work:

70 |

71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 |

gdocs-export:author

Alex

1

150

2

150

85 |

86 |

The following diagram should be on a separate page, to test page break:

87 |

88 |

89 | Diagram showing how work is split 90 |

91 |

Every document needs a diagram!

92 |

93 |

94 |

95 |

96 | Terms and Conditions 97 |

98 |

99 | Staffing 100 |

101 |

Appropriate staff will be used for the execution of this project.

102 |

103 | Relation to Other Agreements 104 |

105 |

This agreement is governed by the same terms and conditions as some other documents.

106 |

107 | Language of Contract 108 |

109 |

Les parties ont convenu de rédiger cette entente en anglais. Parties have decided that this agreement be formulated in English.

110 |

111 |

112 |

113 | Agreement 114 |

115 |

This document is a contract, and contracts must be signed.

116 |

117 |

Signed on behalf of Client LLC:

118 |

119 |

 

120 |

 

121 |
122 |

123 |

Name and title

124 |

 

125 |

 

126 |
127 |

128 |

Signature and date

129 |

130 |

Signed by Adam Smith, President of Vendor Inc.:

131 |

132 |

133 |
134 |

135 |

Signature and date

136 | 137 | 138 | -------------------------------------------------------------------------------- /build/example/template-metadata.tex: -------------------------------------------------------------------------------- 1 | \newcommand{\mytitle}{$title$} 2 | \newcommand{\mysubtitle}{$subtitle$} 3 | \newcommand{\mybody}{$body$} 4 | 5 | 6 | % TODO: support table of key-value metadata in google doc 7 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | test: 2 | override: 3 | - bundle exec rspec -r rspec_junit_formatter --format RspecJunitFormatter -o $CIRCLE_TEST_REPORTS/rspec/junit.xml 4 | -------------------------------------------------------------------------------- /docker/libicu52_52.1-3_amd64.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergachev/gdocs-export/df691eb51646026de0269c2942814afc3dea190c/docker/libicu52_52.1-3_amd64.deb -------------------------------------------------------------------------------- /docker/pandoc-data_1.12.2.1-1build2_all.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergachev/gdocs-export/df691eb51646026de0269c2942814afc3dea190c/docker/pandoc-data_1.12.2.1-1build2_all.deb -------------------------------------------------------------------------------- /docker/pandoc_1.12.2.1-1build2_amd64.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergachev/gdocs-export/df691eb51646026de0269c2942814afc3dea190c/docker/pandoc_1.12.2.1-1build2_amd64.deb -------------------------------------------------------------------------------- /input/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dergachev/gdocs-export/df691eb51646026de0269c2942814afc3dea190c/input/.gitkeep -------------------------------------------------------------------------------- /lib/include/preprocess.rb: -------------------------------------------------------------------------------- 1 | require 'nokogiri' 2 | require 'open-uri' 3 | 4 | class PandocPreprocess 5 | attr_reader :doc, :downloads 6 | def initialize(html) 7 | @source = html 8 | @doc = Nokogiri::HTML(html) 9 | doc.encoding = 'UTF-8' 10 | @downloads = {} 11 | end 12 | 13 | def download_resources 14 | @downloads.each do |path, src| 15 | open(path, 'w') { |f| open(src) { |img| f.write(img.read) }} 16 | end 17 | end 18 | 19 | def html 20 | @doc.to_html 21 | end 22 | 23 | def process 24 | validate 25 | fixup_image_paths 26 | fixup_image_parents 27 | fixup_titles 28 | fixup_span_styles 29 | fixup_headers_footers 30 | fixup_empty_headers 31 | fixup_page_breaks 32 | fixup_lists 33 | end 34 | 35 | # Replace remote with local images 36 | # All image srcs have absolute URLs 37 | def fixup_image_paths 38 | doc.css("img").each do |x| 39 | uri = x['src'] 40 | name = File.basename(uri) 41 | name_with_ext = "#{name}.jpg" 42 | @downloads[name_with_ext] = uri 43 | x['src'] = name_with_ext 44 | end 45 | end 46 | 47 | # Sometimes images are placed inside a heading tag, which crashes latex 48 | def fixup_image_parents 49 | doc.css('h1,h2,h3,h4,h5,h6').>('img').each do |x| 50 | x.parent.replace(x) 51 | end 52 | end 53 | 54 | # Support Google Docs title format, this prepares it for extract_metadata 55 | def fixup_titles 56 | # TODO: ensure neither title or subtitle occur more than once, or are empty 57 | %w[title subtitle].each do |type| 58 | doc.css("p.#{type}").each do |x| 59 | x.replace("

#{x.text}

") 60 | end 61 | end 62 | end 63 | 64 | def fixup_span_styles 65 | # Source has, eg: 66 | # .c14{font-weight:bold} 67 | # Bold Text 68 | # 69 | # Because pandoc doesn't support , we make it into h1.underline 70 | # and rely on custom filtering to convert to LaTeX properly. 71 | styles = { 72 | 'font-weight:bold' => 'strong', 73 | 'font-weight:700' => 'strong', 74 | 'font-style:italic' => 'em', 75 | 'text-decoration:underline' => { class: 'underline' }, 76 | } 77 | 78 | styles.each do |style, repl| 79 | @source.scan(/\.(c\d+)\{([^}]+;)*#{style}[;}]/).each do |cssClass,| 80 | @doc.css("span.#{cssClass}").each do |x| 81 | if Hash === repl 82 | x.replace("#{x.content}") 83 | else 84 | x.name = repl 85 | end 86 | end 87 | end 88 | end 89 | end 90 | 91 | # Replace first/last div with header/footer. 92 | def fixup_headers_footers 93 | @doc.css('div').each do |x| 94 | # header: first div in body 95 | if (!x.previous_sibling && !x.previous_element) 96 | x.replace("

#{x.inner_text}

") 97 | next 98 | end 99 | 100 | # footer: last div in body 101 | if (!x.next_sibling && !x.next_element) 102 | x.replace("

#{x.inner_text}

") 103 | end 104 | end 105 | end 106 | 107 | # Remove empty nodes: Google Docs has lots of them, especially with 108 | # pagebreaks. 109 | def fixup_empty_headers 110 | # must come before pagebreak processing 111 | doc.css('h1,h2,h3,h4,h5,h6').each do |x| 112 | x.remove if x.text =~ /^\s*$/ 113 | end 114 | end 115 | 116 | # Rewrite page breaks into something pandoc can parse 117 | def fixup_page_breaks 118 | #
119 | doc.css('hr[style="page-break-before:always;display:none;"]').each do |x| 120 | x.replace("

") 121 | end 122 | end 123 | 124 | # Get the zero-based depth of a list 125 | def list_depth(list) 126 | klasses = list['class'] or return 0 127 | klass = klasses.split.each do |klass| 128 | md = /^lst-kix_.*-(\d+)$/.match(klass) or next 129 | return md[1].to_i 130 | end 131 | return 0 132 | end 133 | 134 | # Google Docs exports nested lists as separate lists next to each other. 135 | def fixup_lists 136 | # Pass 1: Figure out the depth of each list 137 | depths = [] 138 | @doc.css('ul, ol').each do |list| 139 | depth = list_depth(list) 140 | (depths[depth] ||= []) << list 141 | end 142 | 143 | # Pass 2: In reverse-depth order, coalesce lists 144 | depths.to_enum.with_index.reverse_each do |lists, depth| 145 | next unless lists 146 | lists.reverse_each do |list| 147 | # If the previous item is not a list, we're fine 148 | prev = list.previous_element 149 | next unless prev && prev.respond_to?(:name) && 150 | %w[ol ul].include?(prev.name) 151 | 152 | if list_depth(prev) == depth 153 | # Same depth, append our li's to theirs 154 | prev.add_child(list.children) 155 | list.remove 156 | else 157 | # Lesser depth, append us to their last item 158 | prev.xpath('li').last.add_child(list) 159 | end 160 | end 161 | end 162 | end 163 | 164 | # Detect problems before we try to convert this doc 165 | def validate 166 | @errors = [] 167 | validate_colspan 168 | unless @errors.empty? 169 | STDERR.puts 'Validation errors, bailing' 170 | @errors.each { |e| STDERR.puts e } 171 | exit 1 172 | end 173 | end 174 | 175 | # Detect colspan > 1 176 | def validate_colspan 177 | @doc.css('*[colspan]'). 178 | select { |e| e.attr('colspan').to_i > 1 }.each do |e| 179 | found = true 180 | short = e.text[0, 30] 181 | @errors << "Colspan > 1 for \"#{short}\"" 182 | end 183 | end 184 | end 185 | -------------------------------------------------------------------------------- /lib/pandoc-filter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | import sys 5 | 6 | """ 7 | Pandoc filter to convert all level 2+ headers to paragraphs with 8 | emphasized text. 9 | """ 10 | 11 | from pandocfilters import walk, RawBlock, RawInline, Span, attributes, Str 12 | 13 | def isH1WithClass(key, value, className): 14 | return key == 'Header' and value[0] == 1 and className in value[1][1] 15 | 16 | def isSubTitle(key, value): 17 | return isH1WithClass(key, value, u'ew-pandoc-subtitle') 18 | 19 | def isHeader(key, value): 20 | return isH1WithClass(key, value, u'ew-pandoc-header') 21 | 22 | def isFooter(key, value): 23 | return isH1WithClass(key, value, u'ew-pandoc-footer') 24 | 25 | # hacky workaround for pandoc's not supporting 26 | def isUnderline(key, value): 27 | # if key == 'Span' and value[0][1] == ['underline']: 28 | # sys.stderr.write( json.dumps(value[0]) + "\n") 29 | return key == 'Span' and value[0][1] == ['underline'] 30 | 31 | # see https://github.com/jgm/pandoc/issues/1063 32 | def isTitle(key, value): 33 | return isH1WithClass(key, value, u'ew-pandoc-title') 34 | 35 | def isPageBreak(key, value): 36 | return isH1WithClass(key, value, u'ew-pandoc-pagebreak') 37 | 38 | def isHr(key, value): 39 | return key == 'HorizontalRule' 40 | 41 | # key is a pandoc type, generally "Header", "String" "Para",... 42 | # value is either a string (if key is String) or a list otherwise. 43 | # If a list, value's structure depends on type 44 | # 45 | # If key == 'Header', value will be like: 46 | # 47 | # { 48 | # "c": 49 | # [ 1, 50 | # ["", ["someClass", "someClass2"], []], 51 | # [{"c": "WordOne", "t": "Str"}, {"c": [], "t": "Space"}, {"c": "WordTwo", "t": "Str"}] 52 | # ], 53 | # "t": "Header" 54 | # } 55 | def extract_metadata(key, value, format, meta): 56 | # FIXME: isTitle fails!!!! 57 | if isTitle(key,value): 58 | meta["title"] = { "c": value[2], "t": "MetaInlines" } 59 | return [] 60 | if isSubTitle(key,value): 61 | meta["subtitle"] = { "c": value[2], "t": "MetaInlines" } 62 | return [] 63 | if isHeader(key,value): 64 | meta["header"] = { "c": value[2], "t": "MetaInlines" } 65 | return [] 66 | if isFooter(key,value): 67 | meta["footer"] = { "c": value[2], "t": "MetaInlines" } 68 | return [] 69 | 70 | def fix_hr(key, value, format, meta): 71 | if key == "HorizontalRule": 72 | return RawBlock('latex', '\\hrulefill') 73 | 74 | def fix_pagebreaks(key, value, format, meta): 75 | if isPageBreak(key,value): 76 | return RawBlock('latex', '\\pagebreak') 77 | 78 | def fix_underline(key, value, format, meta): 79 | if isUnderline(key,value): 80 | return [ RawInline('latex', '\\uline{'), Span(value[0], value[1]), RawInline('latex', '}') ] 81 | 82 | def toJSONFilter(): 83 | doc = json.loads(sys.stdin.read()) 84 | if len(sys.argv) > 1: 85 | format = sys.argv[1] 86 | else: 87 | format = "" 88 | 89 | newfmt = 'pandoc-api-version' in doc 90 | 91 | # first, process metadata (title and subtitle) 92 | if newfmt: 93 | result_meta = doc['meta'] 94 | else: 95 | result_meta = doc[0]['unMeta'] 96 | doc = walk(doc, extract_metadata, format, result_meta) 97 | 98 | # We need a title, use a default if unset 99 | if 'title' not in result_meta: 100 | title = {'c': 'Untitled', 't': 'Str'} 101 | result_meta['title'] = { "c": [title], "t": "MetaInlines" } 102 | 103 | if newfmt: 104 | doc['meta'] = result_meta 105 | else: 106 | doc[0]['unMeta'] = result_meta 107 | 108 | # then, fix page breaks 109 | doc = walk(doc, fix_pagebreaks, format, result_meta) 110 | 111 | # then, fix underline 112 | doc = walk(doc, fix_underline, format, result_meta) 113 | 114 | # then, customize horizontal rules (otherwise they're hardcoded in Writers/LaTeX.hs) 115 | doc = walk(doc, fix_hr, format, result_meta) 116 | 117 | json.dump(doc, sys.stdout) 118 | 119 | if __name__ == "__main__": 120 | toJSONFilter() 121 | -------------------------------------------------------------------------------- /lib/pandoc-preprocess.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative 'include/preprocess' 4 | 5 | html = ARGF.read 6 | preproc = PandocPreprocess.new(html) 7 | preproc.process 8 | preproc.download_resources 9 | puts preproc.doc.to_html(encoding: 'ASCII') 10 | -------------------------------------------------------------------------------- /spec/empty_headers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'preprocess' 2 | 3 | RSpec.describe PandocPreprocess, '#fixup_empty_headers' do 4 | it "removes a single empty h1" do 5 | preproc = PandocPreprocess.new('

') 6 | preproc.fixup_empty_headers 7 | expect(preproc.doc.css('h1').size).to eq 0 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/fixtures/sample-doc.html: -------------------------------------------------------------------------------- 1 | Test doc for gd-pandoc

Sample Statement of Work

Every document needs a subtitle...

Prepared by:

Vendor Inc.,
49 Seven square
Montreal, QC H0H 0H0
phone: (514) 345-6789
fax: (514) 345-6789
http://vendor.com

Prepared for:

Client LLC,
37 Prime Lane
Montreal, QC H0H 0H0
phone: (514) 345-6789
fax: (514) 345-6789
http://supplier.com

CONFIDENTIAL: The contents of this document are confidential and are intended exclusively for the designated recipients. Other legal mumbo jumbo….


Agreement

This document is a contract between the writer and the reader.

Signed on behalf of Client LLC:


Name and title


Signature and date

Signed by Adam Smith, President of Vendor Inc.:


Signature and date


1. Background

Every project needs a background. Here’s some lorem ipsum about the project.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sit amet arcu elit. Donec at massa et leo auctor fringilla vitae id erat. Nam feugiat velit vitae ornare tristique. Morbi porttitor, orci vel dapibus malesuada, magna libero ultrices massa, nec interdum diam nisi elementum ipsum. Vestibulum elit lorem, pretium id semper ac, aliquam ut dui. Etiam at magna non nibh viverra tristique. Maecenas consectetur vulputate tellus, ac rutrum felis molestie ut. Aenean tincidunt congue cursus. Maecenas in egestas magna, nec pellentesque justo. Phasellus ante quam, faucibus sit amet tellus et, fringilla convallis eros.

Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra.

Nunc at odio massa. In hac habitasse platea dictumst. Donec posuere erat ac ultrices suscipit. Quisque id velit nulla. Ut in urna orci. Sed at euismod neque. Sed velit velit, fringilla vitae eleifend eget, auctor id mi.


2. Proposed Solution

This wouldn’t be a proposal unless it proposed something. We will re-architect the system into a series of robust robust components:

  1. export, a script to export the data from access, validate it, refactor it into a logical format, then output it in plain-text format.
  2. import, a simple, robust module to handle file uploads of the cleaned data, and efficiently import this data. It will do sufficient error handling to allow full automation.

As we perform the refactoring, we will rewriting the code to ensure it’s documented, easy to test, and easy to update as future needs are identified.

Export component

  • This will be a standalone script responsible for extracting data from source database, and cleaning it up as appropriate.
  • The output format is human readable JSON files, with a logical, easy to understand structure
  • Easy to verify output through manual inspection of output
  • Include comprehensive automated tests to ensure output is correct

Import component

  • Allows non-technical staff to upload already validated JSON files, and sync the target database accordingly.
  • Assumes that input data is relatively safe, as it’s produced via the export component
  • Will be optimized to perform quickly; typical runs will be completed within 60 seconds
  • Improved validation and error handling

Benefits of our solution

Here’s some more lorem ipsum for you all. Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra.

Nunc at odio massa. In hac habitasse platea dictumst. Donec posuere erat ac ultrices suscipit. Quisque id velit nulla. Ut in urna orci. Sed at euismod neque. Sed velit velit, fringilla vitae eleifend eget, auctor id mi.

Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra.

Nullam vitae imperdiet enim. Ut id ultrices leo. In dictum bibendum vestibulum. Donec lobortis condimentum mauris ac interdum. Duis bibendum molestie eleifend. Nunc non neque a neque ornare euismod non ut sapien. Morbi sed diam quis felis commodo vehicula ac et augue. Maecenas pretium justo sit amet imperdiet pharetra.


3. Schedule and Budget

The work described in this mandate will take 300 hours to complete, to be performed within 6-8 weeks of the signing of this agreement.

4. Terms and Conditions

4.1 Staffing

Appropriate staff will be used for the execution of this project.

4.2 Relation to Other Agreements

This agreement is governed by the same terms and conditions as some other documents.

6. Language of Contract

Les parties ont convenu de rédiger cette entente en anglais. Parties have decided that this agreement be formulated in English.

-------------------------------------------------------------------------------- /spec/fixtures/simple-meta.html: -------------------------------------------------------------------------------- 1 | % Alex1 2 | % Alex2 3 | % Alex3 4 | 5 | 6 | 7 | TITLE 8 | SUBTITLE 9 | 10 | 11 |

Sample heading

12 |

Sample line

13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/fixtures/simple-pagebreak.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | FakeTitle FakeTitle 4 | 5 | 6 |

Page One

7 |

Stuff on page one.

8 |
9 |

Page Two

10 |

Stuff on page two.

11 | 12 | 13 | -------------------------------------------------------------------------------- /spec/fixtures/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | FakeTitle FakeTitle 4 | 5 | 6 |

AlexTitle AlexTitle

7 |

AlexSubtitle AlexSubtitle

8 |

Sample heading

9 |

Sample line

10 | 11 | 12 | -------------------------------------------------------------------------------- /spec/fixtures/simple.md: -------------------------------------------------------------------------------- 1 | % Alex 2 | % Alex2 3 | % Alex3 4 | 5 | # Heading l1 6 | ## Heading l2 7 | ### Heading l3 8 | 9 | text with *italics* 10 | -------------------------------------------------------------------------------- /spec/headers_footers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'preprocess' 2 | 3 | RSpec.describe PandocPreprocess, '#fixup_headers_footers' do 4 | it "turns the first div into a header" do 5 | preproc = PandocPreprocess.new('
foo
') 6 | preproc.fixup_headers_footers 7 | expect(preproc.doc.css('h1').size).to eq 1 8 | expect(preproc.doc.at('h1')['class']).to eq 'ew-pandoc-header' 9 | expect(preproc.doc.css('div').size).to eq 0 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/image_parents_spec.rb: -------------------------------------------------------------------------------- 1 | require 'preprocess' 2 | 3 | RSpec.describe PandocPreprocess, '#fixup_image_parents' do 4 | it "unwraps an h1 containing an img" do 5 | preproc = PandocPreprocess.new('

') 6 | preproc.fixup_image_parents 7 | expect(preproc.doc.css('h1').size).to eq 0 8 | expect(preproc.doc.css('body > img').size).to eq 1 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/image_paths_spec.rb: -------------------------------------------------------------------------------- 1 | require 'preprocess' 2 | 3 | RSpec.describe PandocPreprocess, '#fixup_image_paths' do 4 | it "finds a single img" do 5 | preproc = PandocPreprocess.new('') 6 | preproc.fixup_image_paths 7 | expect(preproc.downloads.size).to eq 1 8 | expect(preproc.downloads).to include('foo.jpg') 9 | expect(preproc.downloads['foo.jpg']).to eq 'foo' 10 | expect(preproc.doc.at('img')['src']).to eq 'foo.jpg' 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/nested_lists_spec.rb: -------------------------------------------------------------------------------- 1 | require 'preprocess' 2 | 3 | RSpec.describe PandocPreprocess, '#fixup_lists' do 4 | it "does nothing if there are no lists" do 5 | preproc = PandocPreprocess.new('
foo
') 6 | preproc.fixup_lists 7 | expect(preproc.doc.at('body').inner_html).to eq '
foo
' 8 | end 9 | 10 | it "coalesces consecutive lists" do 11 | preproc = PandocPreprocess.new(< 13 |
  • foo
  • 14 | 15 |
      16 |
    • bar
    • 17 |
    18 | EOF 19 | preproc.fixup_lists 20 | expect(preproc.doc.css('body > ul').size).to eq 1 21 | expect(preproc.doc.css('body > ul > li').size).to eq 2 22 | expect(preproc.doc.css('body > ul > li')[1].inner_text).to eq 'bar' 23 | end 24 | 25 | it "moves sublists into place" do 26 | preproc = PandocPreprocess.new(< 28 |
  • foo
  • 29 | 30 |
      31 |
    • bar
    • 32 |
    33 | EOF 34 | preproc.fixup_lists 35 | expect(preproc.doc.css('body > ul').size).to eq 1 36 | expect(preproc.doc.css('body > ul > li').size).to eq 1 37 | inner = preproc.doc.css('body > ul > li > ul') 38 | expect(inner.size).to eq 1 39 | expect(inner.first.inner_text.strip).to eq 'bar' 40 | end 41 | 42 | it "coalesces consecutive lists" do 43 | preproc = PandocPreprocess.new(< 45 |
  • foo
  • 46 | 47 |
      48 |
    • iggy
    • 49 |
    50 |
      51 |
    • bar
    • 52 |
    53 | EOF 54 | preproc.fixup_lists 55 | doc = preproc.doc 56 | expect(doc.css('body > ul').size).to eq 1 57 | expect(doc.css('body > ul > li').size).to eq 2 58 | expect(doc.css('body > ul > li')[1].inner_text).to eq 'bar' 59 | 60 | inner = doc.css('body > ul > li > ul') 61 | expect(inner.size).to eq 1 62 | expect(inner.first.inner_text.strip).to eq 'iggy' 63 | end 64 | 65 | 66 | it "handles realistic input" do 67 | preproc = PandocPreprocess.new(< 69 | p1 70 |

    71 |

    72 |

    73 | p2 74 |

    75 |

    76 |
      77 |
    • 78 | foo 79 |
    • 80 |
    • 81 | bar 82 |
    • 83 |
    84 |
      85 |
    • 86 | iggy 87 |
    • 88 |
    89 |
      90 |
    • 91 | baz 92 |
    • 93 |
    • 94 | qux 95 |
    • 96 |
    97 |
      98 |
    • 99 | blah 100 |
    • 101 |
    102 |
      103 |
    • 104 | aaa 105 |
    • 106 |
    • 107 | bbb 108 |
    • 109 |
    110 |
      111 |
    • 112 | ccc 113 |
    • 114 |
    115 |

    116 |

    117 | p2 118 |

    119 |

    120 |
      121 |
    1. 122 | ccc 123 |
    2. 124 |
    125 |
      126 |
    1. 127 | ddd 128 |
    2. 129 |
    130 |
      131 |
    1. 132 | eee 133 |
    2. 134 |
    135 |

    136 |

    137 |

    138 |

    139 | fdfafdaf 140 |

    141 | EOF 142 | 143 | # Target result with all attributes stripped, whitespace changes ok 144 | target = <|^)?\s+(<|$)/, '\1\2') 145 |

    p1

    146 |

    p2

    147 |
      148 |
    • foo
    • 149 |
    • bar
        150 |
      • iggy
          151 |
        • baz
        • 152 |
        • qux
        • 153 |
      • 154 |
      • blah
      • 155 |
    • 156 |
    • aaa
    • 157 |
    • bbb
        158 |
      • ccc
      • 159 |
    • 160 |
    161 |

    p2

    162 |
      163 |
    1. ccc
        164 |
      1. ddd
      2. 165 |
    2. 166 |
    3. eee
    4. 167 |
    168 |

    fdfafdaf

    169 | EOF 170 | 171 | preproc.fixup_lists 172 | body = preproc.doc.at('body') 173 | 174 | # Remove attributes, strip space 175 | body.xpath('//*').each do |e| 176 | e.attributes.keys.each { |k| e.remove_attribute(k) } 177 | end 178 | result = body.inner_html.gsub(/(>|^)?\s+(<|$)/, '\1\2') 179 | 180 | expect(result).to eq target 181 | end 182 | end 183 | 184 | -------------------------------------------------------------------------------- /spec/page_breaks_spec.rb: -------------------------------------------------------------------------------- 1 | require 'preprocess' 2 | 3 | RSpec.describe PandocPreprocess, '#fixup_page_breaks' do 4 | it "replaces a single page break with h1" do 5 | preproc = PandocPreprocess.new( 6 | '
    ') 7 | preproc.fixup_page_breaks 8 | expect(preproc.doc.css('hr').size).to eq 0 9 | expect(preproc.doc.css('h1').size).to eq 1 10 | expect(preproc.doc.at('h1')['class']).to eq 'ew-pandoc-pagebreak' 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/span_styles_spec.rb: -------------------------------------------------------------------------------- 1 | require 'preprocess' 2 | 3 | RSpec.describe PandocPreprocess, '#fixup_span_styles' do 4 | it "replaces a bold class with a strong" do 5 | preproc = PandocPreprocess.new(< 7 | 10 | 11 | 12 | foo 13 | 14 | EOF 15 | preproc.fixup_span_styles 16 | expect(preproc.doc.css('span').size).to eq 0 17 | expect(preproc.doc.css('strong').size).to eq 1 18 | expect(preproc.doc.css('strong').text).to eq 'foo' 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib', 'include') 2 | 3 | # This file was generated by the `rspec --init` command. Conventionally, all 4 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 5 | # The generated `.rspec` file contains `--require spec_helper` which will cause 6 | # this file to always be loaded, without a need to explicitly require it in any 7 | # files. 8 | # 9 | # Given that it is always loaded, you are encouraged to keep this file as 10 | # light-weight as possible. Requiring heavyweight dependencies from this file 11 | # will add to the boot time of your test suite on EVERY test run, even for an 12 | # individual file that may not need all of that loaded. Instead, consider making 13 | # a separate helper file that requires the additional dependencies and performs 14 | # the additional setup, and require it from the spec files that actually need 15 | # it. 16 | # 17 | # The `.rspec` file also contains a few flags that are not defaults but that 18 | # users commonly want. 19 | # 20 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 21 | RSpec.configure do |config| 22 | # rspec-expectations config goes here. You can use an alternate 23 | # assertion/expectation library such as wrong or the stdlib/minitest 24 | # assertions if you prefer. 25 | config.expect_with :rspec do |expectations| 26 | # This option will default to `true` in RSpec 4. It makes the `description` 27 | # and `failure_message` of custom matchers include text for helper methods 28 | # defined using `chain`, e.g.: 29 | # be_bigger_than(2).and_smaller_than(4).description 30 | # # => "be bigger than 2 and smaller than 4" 31 | # ...rather than: 32 | # # => "be bigger than 2" 33 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 34 | end 35 | 36 | # rspec-mocks config goes here. You can use an alternate test double 37 | # library (such as bogus or mocha) by changing the `mock_with` option here. 38 | config.mock_with :rspec do |mocks| 39 | # Prevents you from mocking or stubbing a method that does not exist on 40 | # a real object. This is generally recommended, and will default to 41 | # `true` in RSpec 4. 42 | mocks.verify_partial_doubles = true 43 | end 44 | 45 | # The settings below are suggested to provide a good initial experience 46 | # with RSpec, but feel free to customize to your heart's content. 47 | =begin 48 | # These two settings work together to allow you to limit a spec run 49 | # to individual examples or groups you care about by tagging them with 50 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 51 | # get run. 52 | config.filter_run :focus 53 | config.run_all_when_everything_filtered = true 54 | 55 | # Allows RSpec to persist some state between runs in order to support 56 | # the `--only-failures` and `--next-failure` CLI options. We recommend 57 | # you configure your source control system to ignore this file. 58 | config.example_status_persistence_file_path = "spec/examples.txt" 59 | 60 | # Limits the available syntax to the non-monkey patched syntax that is 61 | # recommended. For more details, see: 62 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 63 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 64 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 65 | config.disable_monkey_patching! 66 | 67 | # This setting enables warnings. It's recommended, but in some cases may 68 | # be too noisy due to issues in dependencies. 69 | config.warnings = true 70 | 71 | # Many RSpec users commonly either run the entire suite or an individual 72 | # file, and it's useful to allow more verbose output when running an 73 | # individual spec file. 74 | if config.files_to_run.one? 75 | # Use the documentation formatter for detailed output, 76 | # unless a formatter has already been configured 77 | # (e.g. via a command-line flag). 78 | config.default_formatter = 'doc' 79 | end 80 | 81 | # Print the 10 slowest examples and example groups at the 82 | # end of the spec run, to help surface which specs are running 83 | # particularly slow. 84 | config.profile_examples = 10 85 | 86 | # Run specs in random order to surface order dependencies. If you find an 87 | # order dependency and want to debug it, you can fix the order by providing 88 | # the seed, which is printed after each run. 89 | # --seed 1234 90 | config.order = :random 91 | 92 | # Seed global randomization in this process using the `--seed` CLI option. 93 | # Setting this allows you to use `--seed` to deterministically reproduce 94 | # test failures related to randomization by passing the same `--seed` value 95 | # as the one that triggered the failure. 96 | Kernel.srand config.seed 97 | =end 98 | end 99 | -------------------------------------------------------------------------------- /spec/titles_spec.rb: -------------------------------------------------------------------------------- 1 | require 'preprocess' 2 | 3 | RSpec.describe PandocPreprocess, '#fixup_titles' do 4 | it "annotates a single title" do 5 | preproc = PandocPreprocess.new('

    foo

    ') 6 | preproc.fixup_titles 7 | 8 | expect(preproc.doc.css('p').size).to eq 0 9 | expect(preproc.doc.css('h1').size).to eq 1 10 | 11 | h1 = preproc.doc.at('h1') 12 | expect(h1['class']).to eq 'ew-pandoc-title' 13 | expect(h1.text).to eq 'foo' 14 | end 15 | end 16 | --------------------------------------------------------------------------------