o2c-opendoc-theme 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +2 -0
- data/_includes/directory.html +127 -0
- data/_includes/document-title.txt +57 -0
- data/_includes/toc.html +127 -0
- data/_includes/toolbar.html +68 -0
- data/_includes/welcome.html +5 -0
- data/_layouts/default.html +147 -0
- data/_layouts/home.html +4 -0
- data/_layouts/iframe.html +13 -0
- data/_layouts/page.html +4 -0
- data/_layouts/print.html +27 -0
- data/_sass/_base.scss +398 -0
- data/_sass/_constants.scss +87 -0
- data/_sass/_iframe.scss +7 -0
- data/_sass/_layout.scss +419 -0
- data/_sass/_nav.scss +592 -0
- data/_sass/_print.scss +70 -0
- data/_sass/_syntax-highlighting.scss +61 -0
- data/_sass/_toolbar.scss +372 -0
- data/_sass/_welcome.scss +41 -0
- data/assets/export.md +30 -0
- data/assets/images/chevron-up-white.svg +1 -0
- data/assets/images/chevron-up.svg +1 -0
- data/assets/images/close.svg +17 -0
- data/assets/images/favicon.ico +0 -0
- data/assets/images/feedback-hover.svg +3 -0
- data/assets/images/feedback-mobile.svg +1 -0
- data/assets/images/feedback.svg +1 -0
- data/assets/images/github-hover.svg +3 -0
- data/assets/images/github.svg +1 -0
- data/assets/images/home.svg +14 -0
- data/assets/images/index-img.png +0 -0
- data/assets/images/logo-order2cash.svg +65 -0
- data/assets/images/logo.png +0 -0
- data/assets/images/menu.svg +1 -0
- data/assets/images/opendoc-logo-full.svg +10 -0
- data/assets/images/pdf-hover.svg +11 -0
- data/assets/images/pdf.svg +9 -0
- data/assets/images/search-icon-dark.svg +19 -0
- data/assets/images/search-icon-white.svg +12 -0
- data/assets/images/share.svg +1 -0
- data/assets/images/sidebar-hover.svg +3 -0
- data/assets/images/sidebar.svg +1 -0
- data/assets/images/vertical-dots.svg +1 -0
- data/assets/images/x-mobile.svg +1 -0
- data/assets/index.html +5 -0
- data/assets/js/banner.js +20 -0
- data/assets/js/google_analytics.js +11 -0
- data/assets/js/header.js +31 -0
- data/assets/js/helpers.js +24 -0
- data/assets/js/lunr.min.js +6 -0
- data/assets/js/navigation.js +214 -0
- data/assets/js/page-index.js +57 -0
- data/assets/js/pqueue.js +373 -0
- data/assets/js/pre-loader.js +43 -0
- data/assets/js/search.js +580 -0
- data/assets/js/toolbar.js +144 -0
- data/assets/pdfs/empty +0 -0
- data/assets/siteIndex.json +56 -0
- data/assets/startup/build.sh +41 -0
- data/assets/startup/docprint.html +20 -0
- data/assets/startup/pdf-gen.js +397 -0
- data/assets/startup/prebuild-lunr-index.js +52 -0
- data/assets/styles/main.scss +13 -0
- data/assets/styles/normalize.css +427 -0
- data/assets/vendor/babel-polyfill.min.js +3 -0
- data/assets/vendor/dom4.js +2 -0
- data/assets/vendor/fetch.umd.js +531 -0
- data/assets/vendor/headroom.min.js +7 -0
- data/assets/vendor/jump.min.js +2 -0
- data/assets/vendor/mark.min.js +7 -0
- data/assets/vendor/popper.min.js +5 -0
- data/assets/vendor/web-share-shim.bundle.min.js +2 -0
- metadata +159 -0
@@ -0,0 +1,144 @@
|
|
1
|
+
---
|
2
|
+
---
|
3
|
+
(function () {
|
4
|
+
// Hard coded max-width for mobile view
|
5
|
+
window.isMobileView = function () {
|
6
|
+
return window.innerWidth < 992
|
7
|
+
}
|
8
|
+
|
9
|
+
// Documents - Section toggle
|
10
|
+
|
11
|
+
// Site-nav
|
12
|
+
// --------------------------
|
13
|
+
var menuToggle = document.getElementById('menu-toggle')
|
14
|
+
var showMenu = function showMenu() {
|
15
|
+
menuToggle.checked = true
|
16
|
+
document.body.classList.add('menu-toggled')
|
17
|
+
}
|
18
|
+
var hideMenu = function hideMenu() {
|
19
|
+
menuToggle.checked = false
|
20
|
+
document.body.classList.remove('menu-toggled')
|
21
|
+
}
|
22
|
+
menuToggle.addEventListener('change', function () {
|
23
|
+
if (menuToggle.checked) {
|
24
|
+
showMenu()
|
25
|
+
} else {
|
26
|
+
hideMenu()
|
27
|
+
}
|
28
|
+
})
|
29
|
+
|
30
|
+
var welcomeButton = document.getElementsByClassName('welcome-button')[0]
|
31
|
+
if (welcomeButton) {
|
32
|
+
welcomeButton.onclick = showMenu
|
33
|
+
}
|
34
|
+
|
35
|
+
// Hide site-nav on navigation
|
36
|
+
window.addEventListener('link-click', function () {
|
37
|
+
if (isMobileView()) {
|
38
|
+
hideMenu()
|
39
|
+
}
|
40
|
+
})
|
41
|
+
|
42
|
+
// Edit button
|
43
|
+
// --------------------------
|
44
|
+
var editButtons = document.querySelectorAll('.edit-btn')
|
45
|
+
editButtons.forEach(function (btn) {
|
46
|
+
btn.addEventListener('click', function () {
|
47
|
+
var repoUrl = '{{ site.github.repository_url }}' + '/blob/master/'
|
48
|
+
var page = pageIndex[window.location.pathname]
|
49
|
+
var pageUrl = page ? page.escapedPath : null
|
50
|
+
if (pageUrl) {
|
51
|
+
console.log('opening:', pageUrl)
|
52
|
+
repoUrl += pageUrl
|
53
|
+
}
|
54
|
+
window.open(repoUrl, '_blank')
|
55
|
+
})
|
56
|
+
})
|
57
|
+
|
58
|
+
// Print button
|
59
|
+
// --------------------------
|
60
|
+
var printButtons = document.querySelectorAll('.print-btn')
|
61
|
+
var isProd = '{{ jekyll.environment }}' === 'production'
|
62
|
+
|
63
|
+
printButtons.forEach(function (btn) {
|
64
|
+
btn.addEventListener('click', function () {
|
65
|
+
// S3 folder name; replace slashes to avoid creating sub-folders
|
66
|
+
var replacedRepoName = '{{ site.repository }}'.replace(/\//g, '-') + (isProd ? '' : '-staging')
|
67
|
+
var pdfUrl = '{{ site.offline }}' === 'true' ?
|
68
|
+
'{{ "/assets/pdfs" | relative_url }}' :
|
69
|
+
'https://pdf.opendoc.gov.sg/' + replacedRepoName
|
70
|
+
var page = pageIndex[window.location.pathname]
|
71
|
+
// documentTitle refers to the name of the document folder
|
72
|
+
// If page.dir is slash, that indicates the root directory
|
73
|
+
// PDF at root dir is named export.pdf
|
74
|
+
var documentTitle = page.dir !== '/' ? page.dir : '/export/'
|
75
|
+
if (documentTitle) {
|
76
|
+
pdfUrl += documentTitle.substring(0, documentTitle.length-1) + '.pdf'
|
77
|
+
}
|
78
|
+
window.open(pdfUrl, '_blank')
|
79
|
+
})
|
80
|
+
})
|
81
|
+
|
82
|
+
// Share button
|
83
|
+
// -------------------------
|
84
|
+
var shareButtons = document.querySelectorAll('.share-btn')
|
85
|
+
shareButtons.forEach(function(btn) {
|
86
|
+
btn.addEventListener('click', function() {
|
87
|
+
if (navigator.share) {
|
88
|
+
navigator.share({
|
89
|
+
title: {{ site.title | jsonify }},
|
90
|
+
text: document.title,
|
91
|
+
url: window.location.href
|
92
|
+
}).then()
|
93
|
+
}
|
94
|
+
})
|
95
|
+
})
|
96
|
+
|
97
|
+
// Floating Action Button
|
98
|
+
// -----------------------
|
99
|
+
var floatingActionButtonTrigger = document.getElementById('fab-trigger')
|
100
|
+
var floatingActionButton = document.getElementById('fab')
|
101
|
+
floatingActionButtonTrigger.addEventListener('click', function () {
|
102
|
+
floatingActionButton.classList.toggle('open')
|
103
|
+
});
|
104
|
+
|
105
|
+
var fabOverlay = document.getElementById('fab-overlay')
|
106
|
+
fabOverlay.addEventListener('click', function() {
|
107
|
+
floatingActionButton.classList.remove('open')
|
108
|
+
})
|
109
|
+
|
110
|
+
var backToTopButton = document.getElementById('back-to-top')
|
111
|
+
backToTopButton.addEventListener('click', function() {
|
112
|
+
// jump.js
|
113
|
+
Jump(-(window.pageYOffset || document.documentElement.scrollTop), {
|
114
|
+
duration: 300
|
115
|
+
})
|
116
|
+
})
|
117
|
+
|
118
|
+
// show/hide back-to-top button on scroll and on load
|
119
|
+
function scrollListener() {
|
120
|
+
var scrollTop = window.pageYOffset || document.documentElement.scrollTop
|
121
|
+
if (scrollTop > (window.innerHeight * 2)) {
|
122
|
+
backToTopButton.classList.remove('hidden')
|
123
|
+
} else {
|
124
|
+
backToTopButton.classList.add('hidden')
|
125
|
+
}
|
126
|
+
}
|
127
|
+
|
128
|
+
scrollListener()
|
129
|
+
|
130
|
+
window.addEventListener("scroll", scrollListener)
|
131
|
+
|
132
|
+
// Search Button for mobile
|
133
|
+
// --------------------------
|
134
|
+
var searchButtons = document.querySelectorAll('.search-btn')
|
135
|
+
var searchBoxElement = document.getElementById('search-box')
|
136
|
+
searchButtons.forEach(function(btn) {
|
137
|
+
btn.addEventListener('click', function() {
|
138
|
+
document.body.classList.toggle('search-toggled')
|
139
|
+
if (document.body.classList.contains('search-toggled')) {
|
140
|
+
searchBoxElement.focus()
|
141
|
+
}
|
142
|
+
})
|
143
|
+
})
|
144
|
+
})()
|
data/assets/pdfs/empty
ADDED
File without changes
|
@@ -0,0 +1,56 @@
|
|
1
|
+
---
|
2
|
+
---
|
3
|
+
{% assign index_array = "" | split: ',' %}
|
4
|
+
{%- for page in site.html_pages -%}
|
5
|
+
{%- unless page.exclude -%}
|
6
|
+
{% unless page.name == 'index.html' or page.name == 'index.md' %}
|
7
|
+
{%- assign page_url = page.url -%}
|
8
|
+
{%- assign split_content = page.content | markdownify | split: '<h2' | splice: 1 -%}
|
9
|
+
|
10
|
+
{%- comment -%}Deal with h1 section{%- endcomment -%}
|
11
|
+
{%- assign page_header = split_content.first | split: '</h1>' -%}
|
12
|
+
{%- assign page_header = page_header.first | split: '">' -%}
|
13
|
+
{%- assign page_header = page_header.last | strip_html | newline_to_br | strip_newlines | replace: '<br />', ' ' | replace: '\', '' | strip | smartify | normalize_whitespace -%}
|
14
|
+
{%- assign header_section = '<h1' | append: split_content.first | strip_html | newline_to_br | strip_newlines | replace: '<br />', ' ' | replace: '\', '' | strip | smartify | normalize_whitespace -%}
|
15
|
+
{%- capture item -%}
|
16
|
+
{
|
17
|
+
"title": "{{ page_header}}",
|
18
|
+
"documentTitle": "{%- include_cached document-title.txt dir=page.dir info="title" -%}",
|
19
|
+
"url": "{{page_url | relative_url }}",
|
20
|
+
"text": "{{ header_section }}"
|
21
|
+
}
|
22
|
+
{% endcapture %}
|
23
|
+
{%- assign index_array = index_array | push: item -%}
|
24
|
+
|
25
|
+
{%- comment -%}Then deal with h2 sections {%- endcomment -%}
|
26
|
+
{%- for section in split_content offset:1 -%}
|
27
|
+
{%- assign section_header_html = section | split: 'id="' -%}
|
28
|
+
{%- assign section_header_html = section_header_html[1] | split: '">' -%}
|
29
|
+
|
30
|
+
{%- comment -%}Get section id{%- endcomment -%}
|
31
|
+
{%- assign section_id = section_header_html[0] -%}
|
32
|
+
{%- comment -%}Get section url{%- endcomment -%}
|
33
|
+
{%- assign section_url = page_url | append: '#' | append: section_id -%}
|
34
|
+
|
35
|
+
{%- comment -%}Get section header{%- endcomment -%}
|
36
|
+
{%- assign section_header = section_header_html | shift | join | split: '</h2' -%}
|
37
|
+
{%- assign section_header = section_header[0] | strip_html -%}
|
38
|
+
|
39
|
+
{%- comment -%}Get section body{%- endcomment -%}
|
40
|
+
{%- assign full_section = '<h2 ' | append:section | strip_html | newline_to_br | strip_newlines | replace: '<br />', ' ' | replace: '\', '' | strip | smartify | normalize_whitespace -%}
|
41
|
+
{%- capture item -%}
|
42
|
+
{
|
43
|
+
"title": "{{ section_header | smartify }}",
|
44
|
+
"documentTitle": "{%- include_cached document-title.txt dir=page.dir info="title" -%}",
|
45
|
+
"url": "{{ section_url | relative_url }}",
|
46
|
+
"text": "{{ full_section }}"
|
47
|
+
}
|
48
|
+
{% endcapture %}
|
49
|
+
{%- assign index_array = index_array | push: item -%}
|
50
|
+
|
51
|
+
{%- endfor -%}
|
52
|
+
{%- endunless -%}
|
53
|
+
{%- endunless -%}
|
54
|
+
{%- endfor -%}
|
55
|
+
|
56
|
+
[{{ index_array | join: ','}}]
|
@@ -0,0 +1,41 @@
|
|
1
|
+
---
|
2
|
+
---
|
3
|
+
#!/bin/bash
|
4
|
+
|
5
|
+
echo 'Started script to generate PDFs'
|
6
|
+
echo 'Installing node dependencies'
|
7
|
+
npm i glob@7.1.6 jsdom@16.4.0 js-yaml@4.0.0 p-all@3.0.0
|
8
|
+
if [ "{{ site.offline }}" == "true" ]; then
|
9
|
+
node _site/assets/startup/prebuild-lunr-index.js
|
10
|
+
echo 'Generating lunr index complete'
|
11
|
+
npm i html-pdf@2.2.0
|
12
|
+
else
|
13
|
+
if ! [ -x "$(command -v aws)" ];
|
14
|
+
then
|
15
|
+
echo 'Warning: aws is not installed. Please setup github webhooks for elasticsearch indexing'
|
16
|
+
else
|
17
|
+
APP_NAME="{{ site.repository | split: '/' | last }}"
|
18
|
+
echo "APP_NAME = $APP_NAME"
|
19
|
+
if [ "${AWS_BRANCH}" = "master" ]; then
|
20
|
+
echo "Building prod elasticsearch index for $APP_NAME";
|
21
|
+
aws lambda invoke --function-name es-lambda-prod-es --invocation-type Event \
|
22
|
+
--payload "{\"index\":\"opendocsg-$APP_NAME\", \"repoName\":\"$APP_NAME\"}" /dev/null;
|
23
|
+
fi
|
24
|
+
|
25
|
+
if [ "${AWS_BRANCH}" = "staging" ]; then
|
26
|
+
echo "Building staging elasticsearch index for $APP_NAME";
|
27
|
+
aws lambda invoke --function-name es-lambda-staging-es --invocation-type Event \
|
28
|
+
--payload "{\"index\":\"opendocsg-$APP_NAME\", \"repoName\":\"$APP_NAME\", \"branch\": \"staging\"}" /dev/null;
|
29
|
+
fi
|
30
|
+
|
31
|
+
if [ ! -z "${CUSTOM_BRANCH}" ] && [ "${AWS_BRANCH}" = "${CUSTOM_BRANCH}" ]; then
|
32
|
+
echo "Building $CUSTOM_BRANCH into staging elasticsearch index for $APP_NAME";
|
33
|
+
aws lambda invoke --function-name es-lambda-staging-es --invocation-type Event \
|
34
|
+
--payload "{\"index\":\"opendocsg-$APP_NAME\", \"repoName\":\"$APP_NAME\", \"branch\": \"$CUSTOM_BRANCH\"}" /dev/null;
|
35
|
+
fi
|
36
|
+
fi
|
37
|
+
fi
|
38
|
+
echo "key is $PDF_LAMBDA_KEY"
|
39
|
+
echo "server is $PDF_LAMBDA_SERVER"
|
40
|
+
node _site/assets/startup/pdf-gen.js
|
41
|
+
echo 'End script'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<!-- layout for printing individual documents-->
|
2
|
+
<!DOCTYPE html>
|
3
|
+
<html lang="en">
|
4
|
+
|
5
|
+
<head>
|
6
|
+
<meta charset="utf-8">
|
7
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
9
|
+
|
10
|
+
<link rel="icon" href="./assets/images/favicon.ico">
|
11
|
+
<link rel="stylesheet" href="./assets/styles/normalize.css">
|
12
|
+
<link rel="stylesheet" href="./assets/styles/main.css">
|
13
|
+
</head>
|
14
|
+
|
15
|
+
<body class=print-content>
|
16
|
+
<div id="main-content" class="site-main">
|
17
|
+
</div>
|
18
|
+
</body>
|
19
|
+
|
20
|
+
</html>
|
@@ -0,0 +1,397 @@
|
|
1
|
+
---
|
2
|
+
---
|
3
|
+
const fs = require('fs')
|
4
|
+
const crypto = require('crypto')
|
5
|
+
const pAll = require('p-all')
|
6
|
+
const https = require('https')
|
7
|
+
const glob = require('glob')
|
8
|
+
const path = require('path')
|
9
|
+
const URL = require('url').URL;
|
10
|
+
const jsdom = require('jsdom')
|
11
|
+
const jsyaml = require('js-yaml')
|
12
|
+
const SITE_PATH = __dirname + '/../..'
|
13
|
+
|
14
|
+
const IS_PROD = '{{ jekyll.environment }}' === 'production'
|
15
|
+
const GENERATE_PDF_LOCALLY = '{{ site.offline }}' === 'true' || false
|
16
|
+
const S3_STORAGE_URL = new URL('https://opendoc-theme-pdf.s3-ap-southeast-1.amazonaws.com')
|
17
|
+
|
18
|
+
// For dealing with imagess when baseurl is set, remove leading slashes, if any, for standardization
|
19
|
+
const BASEURL = '{{ site.baseurl }}'.replace('/', '')
|
20
|
+
const LOCAL_PDF_DOLER = path.join(SITE_PATH, 'assets', 'pdfs') // local folder for pdfs
|
21
|
+
// S3 folder; replace slashes to avoid creating sub-folders
|
22
|
+
const S3_PDF_FOLDER = '{{ site.repository }}'.replace(/\//g, '-') + (IS_PROD ? '' : '-staging')
|
23
|
+
|
24
|
+
const BUCKET_NAME = S3_STORAGE_URL.hostname.split('.')[0]
|
25
|
+
|
26
|
+
// CSS to be applied to the PDFs, this will be inserted in <head>
|
27
|
+
const PATH_TO_CSS = path.join(SITE_PATH, 'assets', 'styles', 'main.css')
|
28
|
+
|
29
|
+
// Hash is stored as S3 metadata and served as custom header whenever the pdf is requested
|
30
|
+
const SERIALIZED_HTML_HASH_HEADER = 'x-amz-meta-html-hash'
|
31
|
+
|
32
|
+
// Config.yml file path
|
33
|
+
const CONFIG_YAML_PATH = path.join(SITE_PATH, '..', '_config.yml')
|
34
|
+
|
35
|
+
let pdf
|
36
|
+
let pdfGenConcurrency = 1
|
37
|
+
if (GENERATE_PDF_LOCALLY) {
|
38
|
+
pdf = require('html-pdf')
|
39
|
+
console.log('Generating PDFs and storing locally instead.')
|
40
|
+
} else {
|
41
|
+
if (process.env.PDF_LAMBDA_KEY === undefined ||
|
42
|
+
process.env.PDF_LAMBDA_SERVER === undefined) {
|
43
|
+
console.log('Environment variables PDF_LAMBDA_KEY or PDF_LAMBDA_SERVER for AWS Lambda not present')
|
44
|
+
process.exit(1)
|
45
|
+
}
|
46
|
+
pdfGenConcurrency = process.env.PDF_GEN_CONCURRENCY !== undefined ?
|
47
|
+
parseInt(process.env.PDF_GEN_CONCURRENCY) :
|
48
|
+
50 // Tuned for Netlify
|
49
|
+
console.log(`Generating PDFs on AWS Lambda with concurrency of ${pdfGenConcurrency}.`)
|
50
|
+
console.log(`PDFs will be placed in bucket: ${BUCKET_NAME} in folder ${S3_PDF_FOLDER}.`)
|
51
|
+
}
|
52
|
+
|
53
|
+
// These options are only applied when PDFs are built locally
|
54
|
+
const localPdfOptions = {
|
55
|
+
height: '594mm', // allowed units: mm, cm, in, px
|
56
|
+
width: '420mm',
|
57
|
+
base: 'file://' + SITE_PATH + '/',
|
58
|
+
border: {
|
59
|
+
right: '100px', // default is 0, units: mm, cm, in, px
|
60
|
+
left: '100px',
|
61
|
+
},
|
62
|
+
header: {
|
63
|
+
height: '80px',
|
64
|
+
},
|
65
|
+
footer: {
|
66
|
+
height: '80px',
|
67
|
+
},
|
68
|
+
}
|
69
|
+
|
70
|
+
// List of top-level folder names which may contain html but are not to be printed
|
71
|
+
const printIgnoreFolders = ['assets', 'files', 'iframes', 'images']
|
72
|
+
// List of top-level .html files which are not to be printed
|
73
|
+
const printIgnoreFiles = ['export.html', 'index.html']
|
74
|
+
|
75
|
+
// Tracking statistics
|
76
|
+
let numPdfsStarted = 0
|
77
|
+
let numPdfsUnchanged = 0
|
78
|
+
let numPdfsError = 0
|
79
|
+
let numPdfsSuccess = 0
|
80
|
+
let numTotalPdfs = 0
|
81
|
+
const TIMER = 'Time to create PDFs'
|
82
|
+
|
83
|
+
const main = async () => {
|
84
|
+
// creating exports of individual documents
|
85
|
+
console.time(TIMER)
|
86
|
+
const docFolders = getDocumentFolders(SITE_PATH, printIgnoreFolders)
|
87
|
+
await exportPdfTopLevelDocs(SITE_PATH)
|
88
|
+
await exportPdfDocFolders(SITE_PATH, docFolders)
|
89
|
+
console.log(`PDFs created with success:${numPdfsSuccess} unchanged:${numPdfsUnchanged} error:${numPdfsError} total:${numTotalPdfs}`)
|
90
|
+
console.timeEnd(TIMER)
|
91
|
+
}
|
92
|
+
|
93
|
+
const exportPdfTopLevelDocs = async (sitePath) => {
|
94
|
+
let htmlFilePaths = glob.sync('*.html', { cwd: sitePath })
|
95
|
+
htmlFilePaths = htmlFilePaths.filter((filepath) => !printIgnoreFiles.includes(filepath))
|
96
|
+
htmlFilePaths = htmlFilePaths.map((filepath) => path.join(sitePath, filepath))
|
97
|
+
// Remove folders without HTML files (don't want empty pdfs)
|
98
|
+
if (htmlFilePaths.length === 0) return
|
99
|
+
numTotalPdfs++
|
100
|
+
const orderForFolder = getOrderFromConfig('/')
|
101
|
+
if (orderForFolder && orderForFolder.length) {
|
102
|
+
htmlFilePaths = reorderHtmlFilePaths(htmlFilePaths, orderForFolder)
|
103
|
+
}
|
104
|
+
await createPdf(htmlFilePaths, sitePath, 'export')
|
105
|
+
}
|
106
|
+
|
107
|
+
const exportPdfDocFolders = (sitePath, docFolders) => {
|
108
|
+
const actions = []
|
109
|
+
for (let folder of docFolders) {
|
110
|
+
// find all the folders containing html files
|
111
|
+
const folderPath = path.join(sitePath, folder)
|
112
|
+
let htmlFilePaths = glob.sync('*.html', { cwd: folderPath })
|
113
|
+
htmlFilePaths = htmlFilePaths.filter((filepath) => !printIgnoreFiles.includes(filepath))
|
114
|
+
htmlFilePaths = htmlFilePaths.map((filepath) => path.join(folderPath, filepath))
|
115
|
+
|
116
|
+
// Remove folders without HTML files (don't want empty pdfs)
|
117
|
+
if (htmlFilePaths.length === 0) continue
|
118
|
+
numTotalPdfs++
|
119
|
+
const orderForFolder = getOrderFromConfig(folder)
|
120
|
+
|
121
|
+
if (orderForFolder && orderForFolder.length) {
|
122
|
+
htmlFilePaths = reorderHtmlFilePaths(htmlFilePaths, orderForFolder)
|
123
|
+
}
|
124
|
+
actions.push((() => createPdf(htmlFilePaths, folderPath, folder)))
|
125
|
+
}
|
126
|
+
return pAll(actions, { concurrency: pdfGenConcurrency })
|
127
|
+
}
|
128
|
+
|
129
|
+
// Concatenates the contents in .html files, and outputs export.pdf in the specified output folder
|
130
|
+
const createPdf = (htmlFilePaths, outputFolderPath, documentName) => {
|
131
|
+
logStartedPdf(outputFolderPath)
|
132
|
+
// docprint.html is our template to build pdf up from.
|
133
|
+
const exportHtmlFile = fs.readFileSync(__dirname + '/docprint.html')
|
134
|
+
let cssFile = ''
|
135
|
+
try {
|
136
|
+
cssFile = fs.readFileSync(PATH_TO_CSS)
|
137
|
+
} catch(err) {
|
138
|
+
console.log('Failed to read CSS file at ' + PATH_TO_CSS +', CSS will not be applied')
|
139
|
+
}
|
140
|
+
const exportDom = new jsdom.JSDOM(exportHtmlFile)
|
141
|
+
const exportDomBody = exportDom.window.document.body
|
142
|
+
const exportDomMain = exportDom.window.document.getElementById('main-content')
|
143
|
+
let addedTitle = false
|
144
|
+
let addedDocTitle = false
|
145
|
+
|
146
|
+
htmlFilePaths.forEach(function (filePath) {
|
147
|
+
const file = fs.readFileSync(filePath)
|
148
|
+
const dom = new jsdom.JSDOM(file, {
|
149
|
+
resources: 'usable' // to get JSDOM to load stylesheets
|
150
|
+
})
|
151
|
+
|
152
|
+
// html-pdf can't deal with these
|
153
|
+
removeTagsFromDom(dom, 'script')
|
154
|
+
removeTagsFromDom(dom, 'iframe')
|
155
|
+
inlineImages(dom, outputFolderPath)
|
156
|
+
|
157
|
+
// Site titles needs only be added once
|
158
|
+
if (!addedTitle) {
|
159
|
+
try {
|
160
|
+
const oldTitle = dom.window.document.getElementsByClassName('site-header-text')[0]
|
161
|
+
exportDomBody.insertBefore(oldTitle, exportDomMain)
|
162
|
+
addedTitle = true
|
163
|
+
} catch (error) {
|
164
|
+
console.log('Failed to append Title, skipping: ' + error)
|
165
|
+
}
|
166
|
+
}
|
167
|
+
// Document titles too
|
168
|
+
if (!addedDocTitle) {
|
169
|
+
try {
|
170
|
+
const oldDocTitle = dom.window.document.getElementsByClassName('description-container')[0]
|
171
|
+
exportDomBody.insertBefore(oldDocTitle, exportDomMain)
|
172
|
+
const hr = dom.window.document.createElement('HR')
|
173
|
+
exportDomBody.insertBefore(hr, exportDomMain)
|
174
|
+
addedDocTitle = true
|
175
|
+
} catch (error) {
|
176
|
+
console.log('Failed to append Doc Title, skipping: ' + error)
|
177
|
+
}
|
178
|
+
}
|
179
|
+
|
180
|
+
// Concat all the id:main-content divs
|
181
|
+
try {
|
182
|
+
const oldNode = dom.window.document.getElementById('main-content')
|
183
|
+
exportDomMain.innerHTML += oldNode.innerHTML
|
184
|
+
} catch (error) {
|
185
|
+
console.log('Failed to append Node, skipping: ' + error)
|
186
|
+
}
|
187
|
+
dom.window.close()
|
188
|
+
})
|
189
|
+
const serializedHtmlHash = crypto.createHash('md5').update(exportDom.serialize()).digest('base64')
|
190
|
+
exportDom.window.document.head.innerHTML += '<style>' + cssFile + '</style>'
|
191
|
+
console.log('createpdf hash for:' + outputFolderPath + ': ' + serializedHtmlHash)
|
192
|
+
if (GENERATE_PDF_LOCALLY) {
|
193
|
+
exportDomBody.className += ' print-content-large'
|
194
|
+
// Generate and store locally
|
195
|
+
return new Promise((resolve, reject) => {
|
196
|
+
const url = path.join(LOCAL_PDF_DOLER, documentName + '.pdf')
|
197
|
+
pdf.create(exportDom.serialize(), localPdfOptions).toFile(url, (err, res) => {
|
198
|
+
if (err) {
|
199
|
+
logErrorPdf('Creating PDFs locally', err)
|
200
|
+
return reject()
|
201
|
+
}
|
202
|
+
logSuccessPdf(res.filename)
|
203
|
+
resolve()
|
204
|
+
})
|
205
|
+
exportDom.window.close()
|
206
|
+
})
|
207
|
+
} else {
|
208
|
+
// Apply small font sizes because puppeteer tends to print big
|
209
|
+
exportDomBody.className += ' print-content-small'
|
210
|
+
// Code for this API lives at https://github.com/opendocsg/pdf-lambda
|
211
|
+
const pdfName = `${documentName}.pdf`
|
212
|
+
return new Promise(function (resolve, reject) {
|
213
|
+
// Promise resolves if PDF is present and hash matches. Else reject.
|
214
|
+
const pdfS3Url = S3_STORAGE_URL.toString() + S3_PDF_FOLDER + '/' + pdfName
|
215
|
+
const options = {
|
216
|
+
method: 'HEAD'
|
217
|
+
}
|
218
|
+
const pdfExistsRequest = https.request(pdfS3Url, options, function (res) {
|
219
|
+
if (res.statusCode === 404) {
|
220
|
+
return reject('PDF not present')
|
221
|
+
}
|
222
|
+
if (!(SERIALIZED_HTML_HASH_HEADER in res.headers)) {
|
223
|
+
return reject('HTML hash header not present')
|
224
|
+
}
|
225
|
+
if (res.headers[SERIALIZED_HTML_HASH_HEADER] !== serializedHtmlHash) {
|
226
|
+
return reject('PDF hash does not match')
|
227
|
+
}
|
228
|
+
logUnchangedPdf(pdfName, pdfS3Url)
|
229
|
+
resolve()
|
230
|
+
})
|
231
|
+
pdfExistsRequest.on('error', function (err) {
|
232
|
+
console.log(`pdfExistsRequest encountered error for ${pdfName}:, ${err}`)
|
233
|
+
return reject()
|
234
|
+
})
|
235
|
+
pdfExistsRequest.end()
|
236
|
+
}).then(() => {},
|
237
|
+
function (rejected) {
|
238
|
+
// Rejected: send to lambda function to create PDF
|
239
|
+
const options = {
|
240
|
+
method: 'POST',
|
241
|
+
headers: {
|
242
|
+
'x-api-key': process.env.PDF_LAMBDA_KEY,
|
243
|
+
'content-type': 'application/json',
|
244
|
+
}
|
245
|
+
}
|
246
|
+
|
247
|
+
const pdfCreationBody = {
|
248
|
+
'serializedHTML': exportDom.serialize(),
|
249
|
+
'serializedHTMLName': S3_PDF_FOLDER + '/' + pdfName,
|
250
|
+
'serializedHTMLHash': serializedHtmlHash,
|
251
|
+
'bucketName': BUCKET_NAME
|
252
|
+
}
|
253
|
+
return new Promise(function (resolve, reject) {
|
254
|
+
const pdfCreationRequest = https.request(process.env.PDF_LAMBDA_SERVER, options, function (res) {
|
255
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
256
|
+
logErrorPdf(`pdfCreationRequest status code for ${pdfName}: `, res.statusCode)
|
257
|
+
return reject()
|
258
|
+
}
|
259
|
+
logSuccessPdf(pdfName)
|
260
|
+
return resolve()
|
261
|
+
})
|
262
|
+
pdfCreationRequest.on('error', function(err) {
|
263
|
+
logErrorPdf(`pdfCreationRequest encountered error for ${pdfName}:`, err)
|
264
|
+
return reject()
|
265
|
+
})
|
266
|
+
pdfCreationRequest.write(JSON.stringify(pdfCreationBody))
|
267
|
+
pdfCreationRequest.end()
|
268
|
+
}).catch((error) => {
|
269
|
+
logErrorPdf(`pdfCreation promise error for ${pdfName}`, error)
|
270
|
+
}).finally(() => {
|
271
|
+
exportDom.window.close()
|
272
|
+
})
|
273
|
+
|
274
|
+
})
|
275
|
+
}
|
276
|
+
}
|
277
|
+
|
278
|
+
const logStartedPdf = (outputFolderPath) => {
|
279
|
+
numPdfsStarted++
|
280
|
+
console.log(`createpdf started for:${outputFolderPath} (${numPdfsStarted}/${numTotalPdfs})`)
|
281
|
+
}
|
282
|
+
|
283
|
+
const logUnchangedPdf = (outputFolderPath, pdfUrl) => {
|
284
|
+
numPdfsUnchanged++
|
285
|
+
console.log(`createpdf unchanged for:${outputFolderPath} at ${pdfUrl} (${numPdfsUnchanged}/${numTotalPdfs})`)
|
286
|
+
}
|
287
|
+
|
288
|
+
const logErrorPdf = (origin, error) => {
|
289
|
+
numPdfsError++
|
290
|
+
console.log(`createpdf error for: ${origin}: ${error}(${numPdfsError}/${numTotalPdfs})`)
|
291
|
+
}
|
292
|
+
|
293
|
+
const logSuccessPdf = (outputPdfPath) => {
|
294
|
+
numPdfsSuccess++
|
295
|
+
console.log(`createpdf success for:${outputPdfPath} (${numPdfsSuccess}/${numTotalPdfs})`)
|
296
|
+
}
|
297
|
+
|
298
|
+
const imageType = {
|
299
|
+
'.png':'image/png',
|
300
|
+
'.jpg':'image/jpeg',
|
301
|
+
'.jpeg':'image/jpeg',
|
302
|
+
'.bmp':'image/bmp',
|
303
|
+
'.webp':'image/webp',
|
304
|
+
}
|
305
|
+
|
306
|
+
// Load images and inline them
|
307
|
+
const inlineImages = (dom, outputFolderPath) => {
|
308
|
+
const imgs = dom.window.document.getElementsByTagName('img')
|
309
|
+
for (let i = 0; i < imgs.length; i++) {
|
310
|
+
const img = imgs[i]
|
311
|
+
const originalImagePath = img.src
|
312
|
+
if (!originalImagePath.startsWith('http://') && !originalImagePath.startsWith('https://')) {
|
313
|
+
// Convert all file paths into absolute file paths
|
314
|
+
let imgPath
|
315
|
+
if (originalImagePath.startsWith('/')) {
|
316
|
+
// If baseurl is set, remove baseurl for images to be found
|
317
|
+
if (BASEURL.length > 0) {
|
318
|
+
imgPath = path.join(__dirname, '..', '..', originalImagePath.replace('/' + BASEURL, ''))
|
319
|
+
} else {
|
320
|
+
imgPath = path.join(__dirname, '..', '..', originalImagePath)
|
321
|
+
}
|
322
|
+
} else {
|
323
|
+
// relative path
|
324
|
+
imgPath = path.join(outputFolderPath, originalImagePath).toString()
|
325
|
+
}
|
326
|
+
if (fs.existsSync(imgPath)) {
|
327
|
+
const imgRaw = fs.readFileSync(imgPath)
|
328
|
+
if (path.extname(imgPath) === '.svg') { // don't encode svgs in base64, simply insert them
|
329
|
+
img.src = 'data:image/svg+xml;utf8,' + imgRaw.toString('utf-8')
|
330
|
+
} else {
|
331
|
+
const dataType = imageType[path.extname(imgPath)] || 'image/png'
|
332
|
+
const uri = 'data:' + dataType + ';base64,' + imgRaw.toString('base64')
|
333
|
+
img.src = uri
|
334
|
+
}
|
335
|
+
}
|
336
|
+
}
|
337
|
+
}
|
338
|
+
}
|
339
|
+
|
340
|
+
// Returns a list of the valid document (i.e. folder) paths
|
341
|
+
const getDocumentFolders = (sitePath, printIgnoreFolders) => {
|
342
|
+
return fs.readdirSync(sitePath).filter(function (filePath) {
|
343
|
+
return fs.statSync(path.join(sitePath, filePath)).isDirectory() &&
|
344
|
+
!printIgnoreFolders.includes(filePath)
|
345
|
+
})
|
346
|
+
}
|
347
|
+
|
348
|
+
// Returns true if config file has order for particular folder
|
349
|
+
const getOrderFromConfig = (folderName) => {
|
350
|
+
try {
|
351
|
+
const configYml = yamlToJs(CONFIG_YAML_PATH)
|
352
|
+
const folders = configYml.folders
|
353
|
+
for (folder of folders) {
|
354
|
+
if (folder.name.toLowerCase() === folderName.toLowerCase()) {
|
355
|
+
return folder.order
|
356
|
+
}
|
357
|
+
}
|
358
|
+
return null
|
359
|
+
} catch (error) {
|
360
|
+
return null
|
361
|
+
}
|
362
|
+
}
|
363
|
+
|
364
|
+
|
365
|
+
// Mutates the htmlFilepath array to match order provided in order
|
366
|
+
const reorderHtmlFilePaths = (htmlFilePaths, order) => {
|
367
|
+
const orderedHtmlFilePaths = []
|
368
|
+
for (let i = 0; i < order.length; i++) {
|
369
|
+
const name = path.basename(order[i], '.md')
|
370
|
+
htmlFilePaths.some((filePath) => {
|
371
|
+
if (path.basename(filePath, '.html') === name) {
|
372
|
+
orderedHtmlFilePaths.push(filePath)
|
373
|
+
}
|
374
|
+
})
|
375
|
+
}
|
376
|
+
return orderedHtmlFilePaths
|
377
|
+
}
|
378
|
+
|
379
|
+
// Removes <tag></tag> from dom and everything in between them
|
380
|
+
const removeTagsFromDom = (dom, tagname) => {
|
381
|
+
const tags = dom.window.document.getElementsByTagName(tagname)
|
382
|
+
for (let i = tags.length - 1; i >= 0; i--) {
|
383
|
+
tags[i].parentNode.removeChild(tags[i])
|
384
|
+
}
|
385
|
+
}
|
386
|
+
|
387
|
+
// converts .md to JS Object
|
388
|
+
const markdownToJs = (filepath) => {
|
389
|
+
const configString = fs.readFileSync(filepath).toString().replace(/---/g, '')
|
390
|
+
return jsyaml.load(configString)
|
391
|
+
}
|
392
|
+
|
393
|
+
const yamlToJs = (filepath) => {
|
394
|
+
return jsyaml.load(fs.readFileSync(filepath))
|
395
|
+
}
|
396
|
+
|
397
|
+
main()
|