@thegetty/quire-cli 1.0.0-rc.5 → 1.0.0-rc.50
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.
- package/CHANGELOG.md +154 -7
- package/README.md +15 -0
- package/bin/cli.js +13 -7
- package/package.json +17 -11
- package/patches/README.md +19 -0
- package/patches/install-npm-version+1.0.9.patch +12119 -0
- package/src/Command.js +14 -0
- package/src/commands/README.md +28 -8
- package/src/commands/conf.js +43 -0
- package/src/commands/create.js +22 -10
- package/src/commands/epub.js +1 -2
- package/src/commands/info.js +130 -0
- package/src/commands/pdf.js +60 -10
- package/src/commands/validate.js +64 -0
- package/src/errors/validation/validation-error.js +13 -0
- package/src/errors/validation/yaml-duplicate-error.js +11 -0
- package/src/errors/validation/yaml-parse-error.js +11 -0
- package/src/errors/validation/yaml-validation-error.js +11 -0
- package/src/helpers/clean.js +5 -0
- package/src/helpers/is-empty.js +4 -0
- package/src/helpers/is-quire.js +4 -0
- package/src/helpers/os-utils.js +4 -0
- package/src/helpers/test-cwd.js +4 -0
- package/src/helpers/which.js +4 -0
- package/src/lib/11ty/cli.js +26 -4
- package/src/lib/conf/README.md +104 -0
- package/src/lib/conf/config.js +35 -0
- package/src/lib/conf/defaults.js +37 -0
- package/src/lib/conf/migrations.js +11 -0
- package/src/lib/conf/schema.js +30 -0
- package/src/lib/epub/index.js +3 -1
- package/src/lib/pdf/README.md +11 -4
- package/src/lib/pdf/index.js +7 -5
- package/src/lib/pdf/paged.js +88 -9
- package/src/lib/pdf/pagedPlugin.js +49 -0
- package/src/lib/pdf/prince.js +79 -4
- package/src/lib/pdf/princePlugin.js +35 -0
- package/src/lib/pdf/split.js +61 -0
- package/src/lib/quire/index.js +89 -143
- package/src/main.js +21 -6
- package/src/packageConfig.js +15 -0
- package/src/validators/utils.js +98 -0
- package/src/validators/validate-yaml.js +38 -0
- package/src/lib/config/README.md +0 -3
- package/src/lib/config/index.js +0 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
## CLI Configuration Manager
|
|
2
|
+
|
|
3
|
+
This `quire-cli/lib/config` module manages reading and writing (persisting) options for the Quire CLI using the [`conf`](https://github.com/sindresorhus/conf) package.
|
|
4
|
+
|
|
5
|
+
`conf` stores the config in the system default [user config directory](https://github.com/sindresorhus/env-paths#pathsconfig). For example, on macOS, the config file will be stored in the `~/Library/Preferences/@thegetty/quire-cli` directory.
|
|
6
|
+
|
|
7
|
+
> Changes are written to disk atomically, so if the process crashes during a write, it will not corrupt the existing config.
|
|
8
|
+
|
|
9
|
+
### Configuration
|
|
10
|
+
|
|
11
|
+
`logLevel` The default logging level for the Quire CLI output; default `'info'`.
|
|
12
|
+
|
|
13
|
+
`projectTemplate` A default project starter template to use when creating new projects; default `'quire-starter-default'`.
|
|
14
|
+
|
|
15
|
+
`quirePath` The relative path to `quire-11ty` installed in the project directory; default `./11ty`. When set to `null`, `quire-11ty` is installed to the CLI `lib/quire/versions/<version>/` directory.
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
❯ quire config quire-path '.'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`quireVersion` The default version of `quire-11ty` to install when creating new Quire projects; default `'latest'`.
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
❯ quire config quire-version '1.0.0'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`telemetry` Send anonymous data about Quire usage; default `false`.
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
❯ quire config telemetry --enabled
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
❯ quire config telemetry --disabled
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`updateChannels` A list of distribution tags to use when checking for version updates; default `['latest']`. To show notifications for pre-releases version updates include `'pre-release'` in the array of channels. This can be set using the configuration `--pre-release` command flag.
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
❯ quire config update-channels
|
|
41
|
+
Quire configured to check for updates tagged 'latest'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
❯ quire config update-channels --add 'pre-release'
|
|
46
|
+
Quire configured to check for updates tagged 'latest', 'pre-release'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
❯ quire config update-channels --rm 'pre-release'
|
|
51
|
+
Quire configured to check for updates tagged 'latest'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`updateInterval` Interval at which to check for updates to the Quire CLI and project's `quire-11ty` version; default `'DAILY'`.
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
❯ quire config update-interval
|
|
58
|
+
Quire configured to check for updates DAILY
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
❯ quire config update-interval WEEKLY
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`versionFile ['.quire-version']` The default file name for the `quire-11ty` version file.
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
❯ quire config version-file '.blargh'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Quire CLI `config` Command
|
|
72
|
+
|
|
73
|
+
Running the `config` command without any arguments will start an interactive prompt to configure Quire.
|
|
74
|
+
|
|
75
|
+
To view an individual configuration value and its default value use:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
❯ quire config [key]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
To view the current `logLevel` setting for example:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
❯ quire config logLevel
|
|
85
|
+
loglevel: 'debug' (default 'info')
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
To set an individual configuration value, use the `set` argument:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
❯ quire config set <key> <value>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
To reset *all* keys to their default values, use the `reset` argument:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
❯ quire config reset
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
To reset an individual key to its default value:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
❯ quire config reset [key]
|
|
104
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import Conf from 'conf'
|
|
2
|
+
import defaults from './defaults.js'
|
|
3
|
+
import migrations from './migrations.js'
|
|
4
|
+
import packageConfig from '#src/packageConfig.js'
|
|
5
|
+
import schema from './schema.js'
|
|
6
|
+
|
|
7
|
+
const { name, version } = packageConfig
|
|
8
|
+
|
|
9
|
+
const beforeEachMigration = (store, context) => {
|
|
10
|
+
const { fromVersion, toVersion } = context
|
|
11
|
+
console.info(`quire-cli migrating config from ${fromVersion} → ${toVersion}`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create quire-cli configuration instance
|
|
16
|
+
* @see https://github.com/sindresorhus/conf#confoptions
|
|
17
|
+
*
|
|
18
|
+
* @todo support yaml configuration files
|
|
19
|
+
* https://github.com/sindresorhus/conf?tab=readme-ov-file#can-i-use-yaml-or-another-serialization-format
|
|
20
|
+
*
|
|
21
|
+
* @type {Conf}
|
|
22
|
+
*/
|
|
23
|
+
const config = new Conf({
|
|
24
|
+
beforeEachMigration,
|
|
25
|
+
clearInvalidConfig: true,
|
|
26
|
+
defaults,
|
|
27
|
+
migrations,
|
|
28
|
+
projectName: name,
|
|
29
|
+
projectSuffix: '',
|
|
30
|
+
projectVersion: version,
|
|
31
|
+
schema,
|
|
32
|
+
watch: true,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export default config
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default values for Quire configuration properties
|
|
3
|
+
*
|
|
4
|
+
* Nota bene: these values will overwrite schema default values
|
|
5
|
+
* @see https://github.com/sindresorhus/conf#defaults
|
|
6
|
+
*/
|
|
7
|
+
export default {
|
|
8
|
+
/**
|
|
9
|
+
* Logging level for the Quire CLI output.
|
|
10
|
+
*/
|
|
11
|
+
logLevel: 'info',
|
|
12
|
+
/**
|
|
13
|
+
* Project starter template to use when creating new projects.
|
|
14
|
+
*/
|
|
15
|
+
projectTemplate: 'https://github.com/thegetty/quire-starter-default',
|
|
16
|
+
/**
|
|
17
|
+
* Relative path to quire-11ty installed in the project directory.
|
|
18
|
+
*/
|
|
19
|
+
quire11tyPath: '.',
|
|
20
|
+
/**
|
|
21
|
+
* Version of quire-11ty to install when creating new projects.
|
|
22
|
+
*/
|
|
23
|
+
quireVersion: 'latest',
|
|
24
|
+
/**
|
|
25
|
+
* Npm distribution tag to use when checking for version updates
|
|
26
|
+
*/
|
|
27
|
+
updateChannel: 'rc',
|
|
28
|
+
/**
|
|
29
|
+
* Interval at which to check for updates to the Quire CLI
|
|
30
|
+
* and to quire-11ty version for a prject.
|
|
31
|
+
*/
|
|
32
|
+
updateInterval: 'DAILY',
|
|
33
|
+
/**
|
|
34
|
+
* File name for the quire-11ty version file.
|
|
35
|
+
*/
|
|
36
|
+
versionFile: '.quire',
|
|
37
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quire configuration schema
|
|
3
|
+
* @see https://github.com/sindresorhus/conf#schema
|
|
4
|
+
*
|
|
5
|
+
* Nota bene: schema default values will are overwritten by `defaults.js` values
|
|
6
|
+
* @see https://github.com/sindresorhus/conf#defaults
|
|
7
|
+
*/
|
|
8
|
+
export default {
|
|
9
|
+
logLevel: {
|
|
10
|
+
type: 'string'
|
|
11
|
+
},
|
|
12
|
+
projectTemplate: {
|
|
13
|
+
type: 'string'
|
|
14
|
+
},
|
|
15
|
+
quire11tyPath: {
|
|
16
|
+
type: 'string'
|
|
17
|
+
},
|
|
18
|
+
quireVersion: {
|
|
19
|
+
type: 'string'
|
|
20
|
+
},
|
|
21
|
+
updateChannel: {
|
|
22
|
+
type: 'string'
|
|
23
|
+
},
|
|
24
|
+
updateInterval: {
|
|
25
|
+
type: 'string'
|
|
26
|
+
},
|
|
27
|
+
versionFile: {
|
|
28
|
+
type: 'string'
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/lib/epub/index.js
CHANGED
|
@@ -11,7 +11,9 @@ const __dirname = path.dirname(__filename)
|
|
|
11
11
|
export default async (name = 'epubjs', options = {}) => {
|
|
12
12
|
const lib = { name, options, path }
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const normalizedName = name.replace(/[-_.\s]/g, '').toLowerCase()
|
|
15
|
+
|
|
16
|
+
switch (normalizedName) {
|
|
15
17
|
case 'epubjs': {
|
|
16
18
|
lib.name = 'Epub.js'
|
|
17
19
|
lib.options = {}
|
package/src/lib/pdf/README.md
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
## CLI lib/PDF Module
|
|
2
2
|
|
|
3
|
-
This module
|
|
4
|
-
|
|
3
|
+
This module provides an abstraction to ease PDF generation across PrinceXML and Paged.js. The exported module dynamically loads a wrapper to align the libraries' JS APIs by exporting a single method that accepts a `lib` option and returns an async function that takes input, output, and option params.
|
|
4
|
+
|
|
5
|
+
The module also provides plugins for Prince and Paged.js to map quire webpages to PDF pages. In both cases this is achieved after PDF rendering by querying the HTML document that was printed for `.quire-page` elements and using the PDF generator's APIs to determine content page ids, page data like titles and contributors, and first / last pages. They then use a simple stripping algorithm with `pdf-lib` to split the pages for `--page-pdf` flagged runs.
|
|
5
6
|
|
|
6
7
|
### Paged.js Façade
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
We use [`pagedjs-cli`](https://gitlab.coko.foundation/pagedjs/pagedjs-cli), which adds a headless to paged.js to facilitate serializing to PDF files.
|
|
10
|
+
|
|
11
|
+
For more details on the PDF generating API see the [`Paged.js` documentation](https://gitlab.coko.foundation/pagedjs/). See paged.js's [hooks documentation](https://pagedjs.org/documentation/10-handlers-hooks-and-custom-javascript/) for details on the `afterRendered` hook that quire uses to generate the PDF page map.
|
|
9
12
|
|
|
10
13
|
### Prince XML Façade
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
The Prince abstraction wraps the command line execution of the Prince executable.
|
|
16
|
+
|
|
17
|
+
See the [Prince Command-line Reference](https://www.princexml.com/doc/command-line/). Prince's [scripting documentation](https://www.princexml.com/doc/javascript/) has details on its runtime Javascript implementation, *which is only compatible up to ES5*.
|
|
18
|
+
|
|
19
|
+
The Prince plugin passes page map data as JSON to STDOUT.
|
package/src/lib/pdf/index.js
CHANGED
|
@@ -11,18 +11,20 @@ const __dirname = path.dirname(__filename)
|
|
|
11
11
|
export default async (name = 'pagedjs', options = {}) => {
|
|
12
12
|
const lib = { name, options, path }
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const normalizedName = name.replace(/[-_.\s]/g, '').toLowerCase()
|
|
15
|
+
|
|
16
|
+
switch (normalizedName) {
|
|
15
17
|
case 'paged':
|
|
16
18
|
case 'pagedjs': {
|
|
17
19
|
lib.name = 'Paged.js'
|
|
18
|
-
lib.options =
|
|
20
|
+
lib.options = options
|
|
19
21
|
lib.path = path.join(__dirname, 'paged.js')
|
|
20
22
|
break
|
|
21
23
|
}
|
|
22
24
|
case 'prince':
|
|
23
25
|
case 'princexml': {
|
|
24
26
|
lib.name = 'Prince'
|
|
25
|
-
lib.options =
|
|
27
|
+
lib.options = options
|
|
26
28
|
lib.path = path.join(__dirname, 'prince.js')
|
|
27
29
|
break
|
|
28
30
|
}
|
|
@@ -33,8 +35,8 @@ export default async (name = 'pagedjs', options = {}) => {
|
|
|
33
35
|
|
|
34
36
|
const { default: pdfLib } = await dynamicImport(lib.path)
|
|
35
37
|
|
|
36
|
-
return async (
|
|
38
|
+
return async (publicationInput, coversInput, output) => {
|
|
37
39
|
console.info(`[CLI:lib/pdf] generating PDF using ${lib.name}`)
|
|
38
|
-
return await pdfLib(
|
|
40
|
+
return await pdfLib(publicationInput, coversInput, output, lib.options)
|
|
39
41
|
}
|
|
40
42
|
}
|
package/src/lib/pdf/paged.js
CHANGED
|
@@ -1,19 +1,38 @@
|
|
|
1
1
|
import Printer from 'pagedjs-cli'
|
|
2
|
+
|
|
2
3
|
import fs from 'fs-extra'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import { splitPdf } from './split.js'
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
10
|
+
const __dirname = path.dirname(__filename)
|
|
11
|
+
|
|
12
|
+
// FIXME: This module swallows errors currently.
|
|
3
13
|
|
|
4
14
|
/**
|
|
5
|
-
* A façade module for interacting with Paged.js
|
|
15
|
+
* A façade module for interacting with Paged.js and pagedjs-cli
|
|
6
16
|
* @see https://gitlab.coko.foundation/pagedjs/
|
|
7
17
|
*/
|
|
8
|
-
export default async (
|
|
18
|
+
export default async (publicationInput, coversInput, output, options = {}) => {
|
|
9
19
|
/**
|
|
10
20
|
* Configure the Paged.js Printer options
|
|
11
21
|
* @see https://gitlab.coko.foundation/pagedjs/pagedjs-cli/-/blob/main/src/cli.js
|
|
12
22
|
*/
|
|
23
|
+
|
|
24
|
+
let additionalScripts = []
|
|
25
|
+
|
|
26
|
+
const { pdfConfig } = options
|
|
27
|
+
|
|
28
|
+
additionalScripts.push( path.join(__dirname, 'pagedPlugin.js') )
|
|
29
|
+
|
|
13
30
|
const printerOptions = {
|
|
14
31
|
allowLocal: true,
|
|
15
32
|
debug: options.debug || false,
|
|
16
33
|
enableWarnings: options.debug || false,
|
|
34
|
+
closeAfter: false,
|
|
35
|
+
additionalScripts,
|
|
17
36
|
}
|
|
18
37
|
|
|
19
38
|
if (options.debug) {
|
|
@@ -21,12 +40,12 @@ export default async (input, output, options = {}) => {
|
|
|
21
40
|
console.debug(`[CLI:lib/pdf/pagedjs] Printer options\n${optionsOutput}`)
|
|
22
41
|
}
|
|
23
42
|
|
|
24
|
-
|
|
43
|
+
let printer = new Printer(printerOptions)
|
|
25
44
|
|
|
26
|
-
printer.on('page', (page) => {
|
|
45
|
+
printer.on('page', (page,pageElement,breakToken) => {
|
|
27
46
|
if (page.position === 0) {
|
|
28
47
|
console.info(`[CLI:lib/pdf/pagedjs] loaded`)
|
|
29
|
-
}
|
|
48
|
+
}
|
|
30
49
|
})
|
|
31
50
|
|
|
32
51
|
printer.on('rendered', (msg) => {
|
|
@@ -54,18 +73,78 @@ export default async (input, output, options = {}) => {
|
|
|
54
73
|
}
|
|
55
74
|
|
|
56
75
|
try {
|
|
57
|
-
console.info(`[CLI:lib/pdf/pagedjs] printing ${
|
|
76
|
+
console.info(`[CLI:lib/pdf/pagedjs] printing ${publicationInput}`)
|
|
58
77
|
|
|
59
|
-
const file = await printer.pdf(
|
|
78
|
+
const file = await printer.pdf(publicationInput, pdfOptions)
|
|
60
79
|
.catch((error) => console.error(error))
|
|
61
80
|
|
|
62
|
-
|
|
81
|
+
let pageMap
|
|
82
|
+
|
|
83
|
+
// Now it's printed, create the pageMap by running JS in the printer's context
|
|
84
|
+
let coversFile
|
|
85
|
+
|
|
86
|
+
console.info(`[CLI:lib/pdf/pagedjs] generating page map`)
|
|
87
|
+
const pages = await printer.browser.pages()
|
|
88
|
+
|
|
89
|
+
if (pages.length > 0) {
|
|
90
|
+
pageMap = await pages[pages.length - 1].evaluate(() => {
|
|
91
|
+
// Retrieves the pageMap from our plugin
|
|
92
|
+
return window.pageMap ?? {} // eslint-disable-line no-undef
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if ( pdfConfig?.pagePDF?.coverPage===true && fs.existsSync(coversInput) ) {
|
|
97
|
+
console.info(`[CLI:lib/pdf/pagedjs] printing ${coversInput}`)
|
|
98
|
+
|
|
99
|
+
const coverPrinter = new Printer(printerOptions)
|
|
100
|
+
|
|
101
|
+
coversFile = await coverPrinter.pdf(coversInput, pdfOptions)
|
|
102
|
+
.catch((error) => console.error(error))
|
|
103
|
+
|
|
104
|
+
const coverPages = await coverPrinter.browser.pages()
|
|
105
|
+
|
|
106
|
+
if (coverPages.length > 0) {
|
|
107
|
+
const coversMap = await coverPages[coverPages.length - 1].evaluate(() => {
|
|
108
|
+
// Retrieves the pageMap from our plugin
|
|
109
|
+
return window.pageMap ?? {} // eslint-disable-line no-undef
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
Object.values(coversMap).forEach( cov => {
|
|
113
|
+
if (cov.id in pageMap) {
|
|
114
|
+
pageMap[cov.id].coverPage = cov.startPage
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
coverPrinter.close()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Leave the printer open for debug logs
|
|
124
|
+
if (!options.debug) {
|
|
125
|
+
printer.close()
|
|
126
|
+
}
|
|
63
127
|
|
|
64
128
|
if (file && output) {
|
|
129
|
+
console.info(`[CLI:lib/pdf/pagedjs] writing file(s)`)
|
|
130
|
+
|
|
131
|
+
const { dir } = path.parse(output)
|
|
132
|
+
if (!fs.existsSync(dir)) {
|
|
133
|
+
fs.mkdirsSync(dir)
|
|
134
|
+
}
|
|
135
|
+
|
|
65
136
|
await fs.promises.writeFile(output, file)
|
|
66
137
|
.catch((error) => console.error(error))
|
|
138
|
+
|
|
139
|
+
const files = await splitPdf(file,coversFile,pageMap,options.pdfConfig)
|
|
140
|
+
|
|
141
|
+
Object.entries(files).forEach( async ([filePath,pagePdf]) => {
|
|
142
|
+
await fs.promises.writeFile(filePath,pagePdf)
|
|
143
|
+
.catch((error) => console.error(error))
|
|
144
|
+
})
|
|
67
145
|
}
|
|
146
|
+
|
|
68
147
|
} catch (ERR_FILE_NOT_FOUND) {
|
|
69
|
-
console.error(`[CLI:lib/pdf/pagedjs] file not found ${
|
|
148
|
+
console.error(`[CLI:lib/pdf/pagedjs] file not found ${publicationInput}`)
|
|
70
149
|
}
|
|
71
150
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @class pageTableMapper
|
|
3
|
+
*
|
|
4
|
+
* Responds to [paged.js afterRender hook](https://pagedjs.org/documentation/10-handlers-hooks-and-custom-javascript/) to map the PDF's page table and store it in window.pageMap
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
class pageTableMapper extends Paged.Handler { // eslint-disable-line no-undef
|
|
8
|
+
constructor(chunker, polisher, caller) {
|
|
9
|
+
super(chunker, polisher, caller)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @function afterRendered - fires after all pages are laid out and PDF data is available
|
|
14
|
+
*
|
|
15
|
+
* @param {Array<Page>} pages - Pages rendered from the document
|
|
16
|
+
*/
|
|
17
|
+
afterRendered(pages) {
|
|
18
|
+
let pageMap = {}
|
|
19
|
+
let webpageKey
|
|
20
|
+
|
|
21
|
+
// Iterate pages, build lookup by finding the website page on each PDF page
|
|
22
|
+
for (const p of pages) {
|
|
23
|
+
const quirePageElement = p.element.querySelector('.quire-page')
|
|
24
|
+
|
|
25
|
+
if (!quirePageElement) { continue }
|
|
26
|
+
if (quirePageElement.dataset.pagePdf !== 'true') { continue }
|
|
27
|
+
|
|
28
|
+
const quirePageId = quirePageElement.dataset.id ?? quirePageElement.id
|
|
29
|
+
|
|
30
|
+
if (webpageKey !== quirePageId) {
|
|
31
|
+
webpageKey = quirePageId
|
|
32
|
+
|
|
33
|
+
const title = quirePageElement.dataset.pdfCoverPageTitle
|
|
34
|
+
|
|
35
|
+
let data = { id: webpageKey, startPage: p.position, endPage: p.position, title, }
|
|
36
|
+
|
|
37
|
+
pageMap[webpageKey] = data
|
|
38
|
+
|
|
39
|
+
} else {
|
|
40
|
+
pageMap[webpageKey].endPage = p.position
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
window.pageMap = pageMap // eslint-disable-line no-undef
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Paged.registerHandlers(pageTableMapper) // eslint-disable-line no-undef
|
package/src/lib/pdf/prince.js
CHANGED
|
@@ -1,23 +1,98 @@
|
|
|
1
1
|
import { execa } from 'execa'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import { splitPdf } from './split.js'
|
|
6
|
+
import fs from 'fs-extra'
|
|
7
|
+
|
|
2
8
|
import which from '#helpers/which.js'
|
|
3
9
|
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
11
|
+
const __dirname = path.dirname(__filename)
|
|
12
|
+
|
|
4
13
|
/**
|
|
5
14
|
* A façade module for interacting with the Prince CLI.
|
|
6
15
|
* @see https://www.princexml.com/doc/command-line/
|
|
7
16
|
*/
|
|
8
|
-
export default async (
|
|
17
|
+
export default async (publicationInput, coversInput, output, options = {}) => {
|
|
9
18
|
which('prince')
|
|
10
19
|
|
|
11
20
|
/**
|
|
12
21
|
* @see https://www.princexml.com/doc/command-line/#options
|
|
13
22
|
*/
|
|
23
|
+
|
|
24
|
+
const { pdfConfig } = options
|
|
25
|
+
|
|
26
|
+
// These options run once to get the map pages to essays
|
|
27
|
+
const pageMapOptions = [
|
|
28
|
+
`--script=${ path.join(__dirname, 'princePlugin.js') }`,
|
|
29
|
+
`--output=${output}`,
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
// These options are for the actual user-facing PDF
|
|
14
33
|
const cmdOptions = [
|
|
15
34
|
`--output=${output}`,
|
|
35
|
+
`--pdf-profile=PDF/UA-1`,
|
|
16
36
|
]
|
|
17
37
|
|
|
18
|
-
if (options.debug)
|
|
19
|
-
|
|
38
|
+
if (options.debug) {
|
|
39
|
+
pageMapOptions.push('--debug')
|
|
40
|
+
cmdOptions.push('--debug')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (options.verbose) {
|
|
44
|
+
pageMapOptions.push('--verbose')
|
|
45
|
+
cmdOptions.push('--verbose')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { dir } = path.parse(output)
|
|
49
|
+
if (!fs.existsSync(dir)) {
|
|
50
|
+
fs.mkdirsSync(dir)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Execute the page mapping PDF build
|
|
54
|
+
let pageMap = {}
|
|
55
|
+
try {
|
|
56
|
+
const pageMapOutput = await execa('prince', [...pageMapOptions, publicationInput])
|
|
57
|
+
pageMap = JSON.parse(pageMapOutput.stdout)
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error(`Generating the PDF page map failed with the error ${err.stderr}`)
|
|
60
|
+
process.exit(1)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let coversData
|
|
64
|
+
|
|
65
|
+
if (pdfConfig?.pagePDF?.coverPage === true && fs.existsSync(coversInput)) {
|
|
66
|
+
|
|
67
|
+
const coversPageMapOutput = await execa('prince', [...pageMapOptions, coversInput])
|
|
68
|
+
const coversMap = JSON.parse(coversPageMapOutput.stdout)
|
|
69
|
+
|
|
70
|
+
for (const pageId of Object.keys(coversMap)) {
|
|
71
|
+
if (pageId in pageMap) {
|
|
72
|
+
pageMap[pageId].coverPage = coversMap[pageId].startPage
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
coversData = fs.readFileSync(output,null)
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let stderror,stdout
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
({ stderror, stdout } = await execa('prince', [...cmdOptions, publicationInput]))
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error(`Printing the PDF failed with the error ${err.stderr}`)
|
|
86
|
+
process.exit(1)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const pdfData = fs.readFileSync(output,null)
|
|
90
|
+
|
|
91
|
+
let files = await splitPdf(pdfData,coversData,pageMap,pdfConfig)
|
|
92
|
+
Object.entries(files).forEach( async ([filePath,pagePdf]) => {
|
|
93
|
+
await fs.promises.writeFile(filePath,pagePdf)
|
|
94
|
+
.catch((error) => console.error(error))
|
|
95
|
+
})
|
|
20
96
|
|
|
21
|
-
const { stderror, stdout } = await execa('prince', [...cmdOptions, input])
|
|
22
97
|
return { stderror, stdout }
|
|
23
98
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @function generatePageMap()
|
|
3
|
+
*
|
|
4
|
+
* Walks the PDF-HTML map after printing and serializes the mapped output to STDOUT
|
|
5
|
+
*/
|
|
6
|
+
function generatePageMap() {
|
|
7
|
+
|
|
8
|
+
const els = document.querySelectorAll('.quire-page[data-page-pdf=true]') // eslint-disable-line no-undef
|
|
9
|
+
let pageMap = {}
|
|
10
|
+
|
|
11
|
+
for (let i=0;i < els.length;i++) {
|
|
12
|
+
|
|
13
|
+
const el = els[i]
|
|
14
|
+
const boxes = el.getPrinceBoxes()
|
|
15
|
+
|
|
16
|
+
if (boxes.length < 1) { continue }
|
|
17
|
+
|
|
18
|
+
let data = { id: el.getAttribute('data-id') || el.id, title: el.getAttribute('data-footer-page-title'), startPage: boxes[0].pageNum - 1, endPage: boxes[boxes.length-1].pageNum - 1 }
|
|
19
|
+
|
|
20
|
+
let pageKey = el.getAttribute('data-id') || el.id
|
|
21
|
+
|
|
22
|
+
pageMap[pageKey] = data
|
|
23
|
+
|
|
24
|
+
if ( el.getAttribute('data-pdf-cover-page') !== 'true' ) {
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(JSON.stringify(pageMap))
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Prince.trackBoxes = true // eslint-disable-line no-undef
|
|
35
|
+
Prince.oncomplete = generatePageMap // eslint-disable-line no-undef
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { PDFDocument } from 'pdf-lib'
|
|
4
|
+
import { paths } from '#lib/11ty/index.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @function splitPdf(file,pageMap) -- sections out individual PDFs from `file` according to `pageMap`
|
|
8
|
+
*
|
|
9
|
+
* @param {ArrayBuffer} file - PDF file to split
|
|
10
|
+
* @param {Object} pageMap - page map to split PDf by
|
|
11
|
+
*
|
|
12
|
+
* Creates individual PDFs from by copying `file` (so boxes are already set) and stripping pages out of the range (in reverse order to retain the index sequence)
|
|
13
|
+
* Returns a map of file paths to PDF binary data for serialization
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export async function splitPdf(file,coversFile,pageMap,pdfConfig) {
|
|
17
|
+
|
|
18
|
+
if (!pdfConfig) {
|
|
19
|
+
return {}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { filename, outputDir } = pdfConfig
|
|
23
|
+
|
|
24
|
+
const pdfDoc = await PDFDocument.load(file)
|
|
25
|
+
|
|
26
|
+
const coversDoc = (coversFile !== undefined) ? await PDFDocument.load(coversFile) : undefined
|
|
27
|
+
|
|
28
|
+
let resultFiles = {}
|
|
29
|
+
|
|
30
|
+
for ( const [pageId, pageConfig] of Object.entries(pageMap) ) {
|
|
31
|
+
const { endPage, startPage, coverPage } = pageConfig
|
|
32
|
+
|
|
33
|
+
// TODO: Set the PDF's sectional doc metadata
|
|
34
|
+
|
|
35
|
+
const sectionDoc = await pdfDoc.copy()
|
|
36
|
+
|
|
37
|
+
for (let p=pdfDoc.getPageCount() - 1; p > endPage; --p) {
|
|
38
|
+
sectionDoc.removePage(p)
|
|
39
|
+
}
|
|
40
|
+
for (let q=startPage - 1; q >= 0; --q) {
|
|
41
|
+
sectionDoc.removePage(q)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (coversDoc && coverPage !== undefined && coverPage >= 0) {
|
|
45
|
+
// NB: `copyPages()` sets page sizing + other metadata on the way into the target PDF
|
|
46
|
+
const cover = await sectionDoc.copyPages(coversDoc,[coverPage])
|
|
47
|
+
sectionDoc.insertPage(0,cover[0])
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const section = await sectionDoc.save()
|
|
51
|
+
|
|
52
|
+
const sectionId = pageId.replace(/^page-/g,'')
|
|
53
|
+
const sectionFn = `${filename}-${sectionId}.pdf`
|
|
54
|
+
const sectionFp = path.join( paths.output, outputDir, sectionFn )
|
|
55
|
+
|
|
56
|
+
resultFiles[sectionFp] = section
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return resultFiles
|
|
60
|
+
}
|
|
61
|
+
|