o2c-opendoc-theme 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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()
|