@neo4j-antora/pdf-generator 0.1.0-rc.1
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/README.adoc +131 -0
- package/antora-assembler-pdf.yml +23 -0
- package/package.json +23 -0
- package/pdf-theme/assets/fonts/roboto-mono-latin-400.woff2 +0 -0
- package/pdf-theme/assets/fonts/roboto-mono-latin-500.woff2 +0 -0
- package/pdf-theme/assets/neo4j-logo.svg +1 -0
- package/pdf-theme/print.css +422 -0
- package/scripts/convert.js +125 -0
- package/scripts/setup-vendor.js +65 -0
- package/vendor/extensions/macros-adapter.js +64 -0
- package/vendor/extensions/remote-include-adapter.js +43 -0
- package/vendor/extensions/roles-labels-postprocessor.js +70 -0
- package/vendor/extensions/table-footnotes-postprocessor.js +74 -0
package/README.adoc
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
= PDF generation
|
|
2
|
+
|
|
3
|
+
Generates a PDF export from any Antora docset, via `@antora/assembler` +
|
|
4
|
+
`@antora/pdf-extension` and `asciidoctor-web-pdf` (a Chrome/Puppeteer + CSS
|
|
5
|
+
renderer) instead of the legacy Gradle + AsciidoctorJ mono-merge pipeline.
|
|
6
|
+
See `reusable-pdf-build.yml` in `.github/workflows/` for the CI entry point
|
|
7
|
+
that uses this package.
|
|
8
|
+
|
|
9
|
+
Published as a normal npm dependency - a docset adds it as a devDependency
|
|
10
|
+
and gets a real, versioned copy in its own `node_modules`, the same way it
|
|
11
|
+
already depends on `@neo4j-antora/roles-labels` or `@neo4j-antora/tabbed-nav`.
|
|
12
|
+
There's nothing to check out, symlink, or hand-configure: `@antora/pdf-
|
|
13
|
+
extension` is this package's own dependency (not something a docset lists
|
|
14
|
+
itself), so `npm install @neo4j-antora/pdf-generator` is the entire setup.
|
|
15
|
+
|
|
16
|
+
== Usage
|
|
17
|
+
|
|
18
|
+
A docset needs its own small playbook (like `preview.yml`/`publish.yml`,
|
|
19
|
+
since content sources/attributes differ per docset - not shared here) that
|
|
20
|
+
registers `@antora/pdf-extension` with this package's assembler config:
|
|
21
|
+
|
|
22
|
+
[source,yaml]
|
|
23
|
+
----
|
|
24
|
+
antora:
|
|
25
|
+
extensions:
|
|
26
|
+
- require: '@antora/pdf-extension'
|
|
27
|
+
config_file: './node_modules/@neo4j-antora/pdf-generator/antora-assembler-pdf.yml'
|
|
28
|
+
----
|
|
29
|
+
|
|
30
|
+
and a `package.json` script to run it, e.g. `"pdf": "antora pdf.yml"`.
|
|
31
|
+
|
|
32
|
+
== Layout
|
|
33
|
+
|
|
34
|
+
- `antora-assembler-pdf.yml` - Assembler config, pointed at from a docset's
|
|
35
|
+
own playbook (see above).
|
|
36
|
+
- `scripts/convert.js` - wraps the real `asciidoctor-web-pdf` binary as the
|
|
37
|
+
assembler's `build.command`. Resolves the renderer binary (via
|
|
38
|
+
`require.resolve` against its own `asciidoctor-pdf` dependency, regardless
|
|
39
|
+
of how npm hoists it) and the stylesheet/extension paths (via `__dirname`)
|
|
40
|
+
at runtime, so nothing here is hardcoded to a particular install layout.
|
|
41
|
+
Also works around a real bug (`ifndef::backend-pdf[]` not evaluating
|
|
42
|
+
correctly under this pipeline) for docsets using that idiom - a no-op for
|
|
43
|
+
any docset that doesn't.
|
|
44
|
+
- `pdf-theme/print.css` - print stylesheet targeting Asciidoctor's standard
|
|
45
|
+
HTML5 backend class names (`sect1`, `admonitionblock`, `listingblock`,
|
|
46
|
+
`tableblock`, `label`, ...), using colour/font tokens copied from
|
|
47
|
+
`docs-ui`'s `src/css/vars.css` (itself resolving to raw Needle design
|
|
48
|
+
system tokens in `@neo4j-ndl/base`'s `tokens/css/tokens.css`) and
|
|
49
|
+
`docs-ui`'s `src/css/labels.css`. Not a fork of the real site CSS wholesale
|
|
50
|
+
- Asciidoctor's default HTML5 output doesn't have Antora's `.doc` wrapper
|
|
51
|
+
markup most of the site CSS's selectors are written against, so most of
|
|
52
|
+
it wouldn't match anything here anyway. Fonts are vendored into
|
|
53
|
+
`pdf-theme/assets/fonts/` rather than read from a docset's own HTML
|
|
54
|
+
build output, so this theme has no dependency on that build having
|
|
55
|
+
already run.
|
|
56
|
+
- `extensions/` - `roles-labels-postprocessor.js` and
|
|
57
|
+
`table-footnotes-postprocessor.js` port the essential parts of the Antora
|
|
58
|
+
extensions of the same name (see "Known limitations" below);
|
|
59
|
+
`macros-adapter.js` and `remote-include-adapter.js` wrap the real
|
|
60
|
+
`@neo4j-documentation/macros`/`remote-include` packages, fixing a
|
|
61
|
+
pre-4.x-Asciidoctor.js API incompatibility in each at runtime (see their
|
|
62
|
+
own comments) rather than patching the installed packages - a nested
|
|
63
|
+
dependency's own `postinstall` isn't guaranteed to run once this package
|
|
64
|
+
is itself installed as a docset's dependency, so `patch-package` isn't a
|
|
65
|
+
reliable option here the way it is for a top-level project.
|
|
66
|
+
- `@asciidoctor/core` 4.x (needed by `asciidoctor-pdf`) vs. Antora's own 2.x:
|
|
67
|
+
no longer needs the isolated sibling npm project the git-checkout-based
|
|
68
|
+
version of this pipeline required - both land in the same install
|
|
69
|
+
correctly because npm nests conflicting versions of the same dependency
|
|
70
|
+
automatically; this package simply declares its own real dependencies and
|
|
71
|
+
lets normal npm resolution do the isolation.
|
|
72
|
+
|
|
73
|
+
== Known limitations
|
|
74
|
+
|
|
75
|
+
- **No footnotes render at all, anywhere in the PDF, because of the TOC.**
|
|
76
|
+
Isolated by direct testing against `asciidoctor-web-pdf` outside this whole
|
|
77
|
+
pipeline: passing `-a toc` alone (nothing else - no tables, no our own
|
|
78
|
+
extensions) is enough to break native Asciidoctor `footnote:[...]`
|
|
79
|
+
conversion entirely - no superscript reference, no `#footnotes` div, just
|
|
80
|
+
the footnote text dropped in-line as if the macro had never been
|
|
81
|
+
processed. Since the print theme enables `toc` for every docset, this
|
|
82
|
+
currently affects every PDF this pipeline produces, not just docsets using
|
|
83
|
+
footnotes inside tables. `extensions/table-footnotes-postprocessor.js`
|
|
84
|
+
(see above) is correctly written and wired in, but has nothing to do while
|
|
85
|
+
this is broken - there's no `#footnotes` div for it to move content out of.
|
|
86
|
+
Not yet root-caused further (asciidoctor-web-pdf itself, or its Vivliostyle
|
|
87
|
+
dependency) or fixed.
|
|
88
|
+
- `@neo4j-antora/roles-labels` and `@neo4j-antora/table-footnotes` are Antora
|
|
89
|
+
extensions (hook `pagesComposed`, operate on an Antora `ContentCatalog`) -
|
|
90
|
+
neither can run against this pipeline at all, structurally: the assembler
|
|
91
|
+
hands the merged `.adoc` to a completely separate, isolated Asciidoctor
|
|
92
|
+
conversion with no Antora generator context. `table-footnotes` is ported
|
|
93
|
+
as a plain Asciidoctor `Postprocessor` instead
|
|
94
|
+
(`extensions/table-footnotes-postprocessor.js`) - pure HTML restructuring,
|
|
95
|
+
no Antora-specific data needed, so the port is a straight copy.
|
|
96
|
+
`roles-labels-postprocessor.js` reuses `@neo4j-antora/roles-labels`'s own
|
|
97
|
+
shared `lib/process-labels.js` core directly (same synonym resolution,
|
|
98
|
+
version/product-suffix stripping, dataset attributes as the real HTML
|
|
99
|
+
site - not a hand-ported subset), with one deliberate behavioural
|
|
100
|
+
difference: it passes `skipDiscrete: false`, because `@antora/assembler`
|
|
101
|
+
marks *every* merged page's heading `discrete` to flatten section nesting
|
|
102
|
+
across the book, which isn't the authorial signal `discrete` is on a real
|
|
103
|
+
Antora HTML page (see `process-labels.js`'s own comment on that option).
|
|
104
|
+
- Classification of `reusable-docs-build.yml`'s default `antora-extensions`
|
|
105
|
+
list, checked against their actual registration mechanism:
|
|
106
|
+
`aliases-redirects`, `antora-modify-sitemaps`, `antora-page-list` all hook
|
|
107
|
+
Antora-only lifecycle events (site redirects, sitemap.xml, the HTML
|
|
108
|
+
page-list artifact) - inherently HTML/site-only, correctly and harmlessly
|
|
109
|
+
absent from PDF output, nothing to do. `antora-unlisted-pages`/
|
|
110
|
+
`selector-labels` not individually confirmed but almost certainly the same
|
|
111
|
+
(nav/site-UI concerns). `xref-hash-validator` hooks `contentClassified`/
|
|
112
|
+
`documentsConverted` but only validates/warns - doesn't transform content,
|
|
113
|
+
so its absence from PDF just means xrefs aren't validated in that context,
|
|
114
|
+
not a rendering gap.
|
|
115
|
+
- **TODO, not yet decided**: `@neo4j-antora/mark-terms` *is* a plain
|
|
116
|
+
Asciidoctor extension (`module.exports = function (registry) {...}`, no
|
|
117
|
+
Antora event hook) - structurally it'd just need `--extension`
|
|
118
|
+
registration, the same easy fix as `macros`/`remote-include`. Not done
|
|
119
|
+
yet, because it's a product decision, not just a technical one: mark-terms
|
|
120
|
+
adds a trademark/copyright mark on a term's *first use per page* (e.g. the
|
|
121
|
+
first "Neo4j" on each HTML page) - a PDF has no equivalent notion of "page"
|
|
122
|
+
the way a docset's HTML pages do, so "first use" would need redefining
|
|
123
|
+
(per PDF? per chapter? just once, ever?), or the whole approach swapped for
|
|
124
|
+
a single boilerplate copyright/trademark statement somewhere in the PDF
|
|
125
|
+
(e.g. the cover or a colophon page) instead of inline marks. Needs a call
|
|
126
|
+
on the right behaviour before doing the (otherwise straightforward)
|
|
127
|
+
registration work.
|
|
128
|
+
- This package's `print.css` label colours are hand-resolved from
|
|
129
|
+
`docs-ui`'s source files, not read from them live - if the Needle tokens or
|
|
130
|
+
`docs-ui`'s `vars.css`/`labels.css` change, this drifts until someone
|
|
131
|
+
notices and re-syncs it by hand.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
component_version_filter:
|
|
2
|
+
names: '**'
|
|
3
|
+
assembly:
|
|
4
|
+
attributes:
|
|
5
|
+
allow-uri-read: ''
|
|
6
|
+
toc: ''
|
|
7
|
+
toc-title: Table of Contents
|
|
8
|
+
toclevels: 2
|
|
9
|
+
build:
|
|
10
|
+
# build.cwd (what a relative command resolves against) defaults to the
|
|
11
|
+
# *playbook's* directory (docs-dir) - the same directory `npm install` is
|
|
12
|
+
# run from for that docset, since that's where its own package.json lives.
|
|
13
|
+
# node_modules is therefore always right here, at a fixed, predictable
|
|
14
|
+
# depth, regardless of how deeply nested docs-dir is within the docset
|
|
15
|
+
# repo - unlike the old git-checkout-into-.pdf-tools convention this
|
|
16
|
+
# package replaces, no per-consumer path rewriting is needed. Once
|
|
17
|
+
# convert.js itself is running, it resolves everything else (the
|
|
18
|
+
# renderer, the stylesheet, the extensions) from its own location or via
|
|
19
|
+
# require.resolve against its own dependencies instead - see this
|
|
20
|
+
# package's own README.
|
|
21
|
+
command: node ./node_modules/@neo4j-antora/pdf-generator/scripts/convert.js --trace
|
|
22
|
+
keep_source: true
|
|
23
|
+
qualify_exports: true
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neo4j-antora/pdf-generator",
|
|
3
|
+
"version": "0.1.0-rc.1",
|
|
4
|
+
"description": "Generates a PDF export from an Antora docset, styled to match the real Neo4j docs site, via @antora/assembler + @antora/pdf-extension and asciidoctor-web-pdf",
|
|
5
|
+
"main": "scripts/convert.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
8
|
+
"postinstall": "node scripts/setup-vendor.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"antora",
|
|
12
|
+
"pdf"
|
|
13
|
+
],
|
|
14
|
+
"author": "Neo4j",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@antora/pdf-extension": "1.0.0",
|
|
18
|
+
"@neo4j-antora/roles-labels": "0.1.14-rc.1",
|
|
19
|
+
"@neo4j-documentation/macros": "1.0.4",
|
|
20
|
+
"@neo4j-documentation/remote-include": "1.0.0",
|
|
21
|
+
"node-html-parser": "9.0.4"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 75"><path d="M39.23 19c-10.58 0-17.68 6.16-17.68 18.11v8.52a8 8 0 013.45-.82 7.89 7.89 0 013.46.8v-8.54c0-7.75 4.28-11.73 10.8-11.73S50 29.32 50 37.07v18.62h6.89V37.07C56.91 25.05 49.81 19 39.23 19zm21.43 18.8c0-10.87 8-18.84 19.27-18.84s19.13 8 19.13 18.84v2.53H67.9c1 6.38 5.8 9.93 12 9.93 4.64 0 7.9-1.45 10-4.56h7.6c-2.75 6.66-9.27 10.94-17.6 10.94-11.27 0-19.24-7.97-19.24-18.84zm31.15-3.62c-1.38-5.73-6.08-8.84-11.88-8.84s-10.43 3.19-11.81 8.84zm10.93 3.62c0-10.86 8-18.83 19.27-18.83s19.27 8 19.27 18.83-8 18.84-19.27 18.84-19.27-7.97-19.27-18.84zm31.59 0c0-7.24-4.93-12.46-12.32-12.46S109.7 30.56 109.7 37.8s4.92 12.46 12.3 12.46 12.33-5.21 12.33-12.46zm46.31 25.02h.8c4.42 0 6.08-2 6.08-7V20.16h6.89v35.2c0 8.84-3.48 13.4-12.32 13.4h-1.45zm-3.44-3.68h-6.89v-8.49h-17.45A8.64 8.64 0 01145 46.2a7.72 7.72 0 01.94-8.16l15.66-20.55a8.65 8.65 0 0115.6 5.13v21.92h5.17v6.11h-5.17zM151.67 41.8a1.76 1.76 0 00-.32 1 1.72 1.72 0 001.73 1.73h17.23V22.45a1.7 1.7 0 00-1.19-1.68 2.36 2.36 0 00-.63-.09 1.63 1.63 0 00-1.36.73L151.67 41.8z"/><path d="M191 5.53a5.9 5.9 0 105.89 5.9 5.9 5.9 0 00-5.89-5.9zM24.7 47a5.84 5.84 0 00-3.54 1.2l-6.48-4.43a6 6 0 00.22-1.59A5.89 5.89 0 109 48a5.81 5.81 0 003.54-1.2L19 51.26a5.89 5.89 0 000 3.19l-6.48 4.43A5.81 5.81 0 009 57.68a5.9 5.9 0 105.89 5.89 6 6 0 00-.21-1.57l6.48-4.43a5.84 5.84 0 003.54 1.2 5.9 5.9 0 000-11.77z" fill="#018bff"/></svg>
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/* Print-specific stylesheet shared by every docset's PDF export (see
|
|
2
|
+
docs-tools/pdf/README - checked out into each repo's build by
|
|
3
|
+
reusable-pdf-build.yml). Reuses brand tokens read from the real site.css
|
|
4
|
+
(build/site/assets/css/site.css) rather than forking that file, since
|
|
5
|
+
Asciidoctor's HTML5 backend (used by asciidoctor-web-pdf) does not produce
|
|
6
|
+
Antora's `.doc` wrapper markup.
|
|
7
|
+
|
|
8
|
+
Every asset path below is relative to this file's own location, not to
|
|
9
|
+
whatever repo/cwd the build runs in - a browser resolves CSS url()s
|
|
10
|
+
against the stylesheet's own file:// location, so this stays correct
|
|
11
|
+
wherever docs-tools gets checked out. Font files are vendored into
|
|
12
|
+
assets/fonts/ here (not read from a docset's own build output) so this
|
|
13
|
+
theme has no dependency on that docset's HTML build having already run. */
|
|
14
|
+
@font-face {
|
|
15
|
+
font-family: 'Roboto Mono';
|
|
16
|
+
font-weight: 400;
|
|
17
|
+
src: url('assets/fonts/roboto-mono-latin-400.woff2') format('woff2');
|
|
18
|
+
}
|
|
19
|
+
@font-face {
|
|
20
|
+
font-family: 'Roboto Mono';
|
|
21
|
+
font-weight: 500;
|
|
22
|
+
src: url('assets/fonts/roboto-mono-latin-500.woff2') format('woff2');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
:root {
|
|
26
|
+
--brand-primary: #0a6190;
|
|
27
|
+
--brand-primary-strong: #02507b;
|
|
28
|
+
--brand-primary-weak: #e7fafb;
|
|
29
|
+
--brand-text: #1a1b1d;
|
|
30
|
+
--brand-text-weak: #4d5157;
|
|
31
|
+
--brand-text-weaker: #5e636a;
|
|
32
|
+
--brand-border: #e2e3e5;
|
|
33
|
+
--brand-border-strong: #bbbec3;
|
|
34
|
+
--brand-bg-weak: #fff;
|
|
35
|
+
--brand-bg-strong: #f5f6f6;
|
|
36
|
+
--brand-danger: #a6291f;
|
|
37
|
+
--brand-danger-bg: #fdf1ef;
|
|
38
|
+
--brand-warning: #8a5a00;
|
|
39
|
+
--brand-warning-bg: #fdf6e8;
|
|
40
|
+
--font-body: 'Public Sans', 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
41
|
+
--font-mono: 'Roboto Mono', Menlo, Monaco, Consolas, 'Courier New', monospace;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@page {
|
|
45
|
+
size: A4;
|
|
46
|
+
margin: 25mm 18mm 20mm 18mm;
|
|
47
|
+
}
|
|
48
|
+
/* Verso (even/left) pages: page number on the outer (left) edge */
|
|
49
|
+
@page :left {
|
|
50
|
+
@bottom-left {
|
|
51
|
+
content: counter(page);
|
|
52
|
+
font-family: var(--font-body);
|
|
53
|
+
font-size: 9pt;
|
|
54
|
+
color: var(--brand-text-weaker);
|
|
55
|
+
}
|
|
56
|
+
@bottom-right {
|
|
57
|
+
content: string(doc-title) " " string(doc-version);
|
|
58
|
+
font-family: var(--font-body);
|
|
59
|
+
font-size: 9pt;
|
|
60
|
+
color: var(--brand-text-weaker);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/* Recto (odd/right) pages: page number on the outer (right) edge */
|
|
64
|
+
@page :right {
|
|
65
|
+
@bottom-left {
|
|
66
|
+
content: string(doc-title) " " string(doc-version);
|
|
67
|
+
font-family: var(--font-body);
|
|
68
|
+
font-size: 9pt;
|
|
69
|
+
color: var(--brand-text-weaker);
|
|
70
|
+
}
|
|
71
|
+
@bottom-right {
|
|
72
|
+
content: counter(page);
|
|
73
|
+
font-family: var(--font-body);
|
|
74
|
+
font-size: 9pt;
|
|
75
|
+
color: var(--brand-text-weaker);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
@page :first {
|
|
79
|
+
@bottom-left {
|
|
80
|
+
content: normal;
|
|
81
|
+
}
|
|
82
|
+
@bottom-right {
|
|
83
|
+
content: normal;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#cover.title-page h1 {
|
|
88
|
+
string-set: doc-title content();
|
|
89
|
+
}
|
|
90
|
+
#cover .details #revnumber {
|
|
91
|
+
string-set: doc-version content();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
body.book {
|
|
95
|
+
font-family: var(--font-body);
|
|
96
|
+
color: var(--brand-text);
|
|
97
|
+
font-size: 10.5pt;
|
|
98
|
+
line-height: 1.5;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#cover.title-page {
|
|
102
|
+
text-align: left;
|
|
103
|
+
page-break-after: always;
|
|
104
|
+
}
|
|
105
|
+
#cover.title-page::before {
|
|
106
|
+
content: url('assets/neo4j-logo.svg');
|
|
107
|
+
display: block;
|
|
108
|
+
width: 110pt;
|
|
109
|
+
}
|
|
110
|
+
#cover.title-page h1 {
|
|
111
|
+
font-size: 32pt;
|
|
112
|
+
font-weight: 700;
|
|
113
|
+
color: var(--brand-primary-strong);
|
|
114
|
+
margin-top: 30%;
|
|
115
|
+
border-bottom: 2pt solid var(--brand-primary);
|
|
116
|
+
padding-bottom: 12pt;
|
|
117
|
+
}
|
|
118
|
+
#cover .details {
|
|
119
|
+
color: var(--brand-text-weak);
|
|
120
|
+
font-size: 11pt;
|
|
121
|
+
margin-top: 8pt;
|
|
122
|
+
}
|
|
123
|
+
#cover #revdate {
|
|
124
|
+
display: none;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Table of contents */
|
|
128
|
+
#toc {
|
|
129
|
+
page-break-after: always;
|
|
130
|
+
}
|
|
131
|
+
#toctitle {
|
|
132
|
+
font-family: var(--font-body);
|
|
133
|
+
color: var(--brand-primary-strong);
|
|
134
|
+
font-size: 22pt;
|
|
135
|
+
font-weight: 700;
|
|
136
|
+
border-bottom: 2pt solid var(--brand-primary);
|
|
137
|
+
padding-bottom: 10pt;
|
|
138
|
+
margin-bottom: 18pt;
|
|
139
|
+
}
|
|
140
|
+
#toc ul {
|
|
141
|
+
font-family: var(--font-body);
|
|
142
|
+
list-style-type: none;
|
|
143
|
+
margin-left: 0;
|
|
144
|
+
padding-left: 0;
|
|
145
|
+
}
|
|
146
|
+
#toc ul.sectlevel0 {
|
|
147
|
+
margin-left: 0;
|
|
148
|
+
}
|
|
149
|
+
#toc ul ul.sectlevel1 {
|
|
150
|
+
margin-left: 14pt;
|
|
151
|
+
}
|
|
152
|
+
#toc li {
|
|
153
|
+
margin-top: 6pt;
|
|
154
|
+
line-height: 1.4;
|
|
155
|
+
}
|
|
156
|
+
#toc a {
|
|
157
|
+
display: flex;
|
|
158
|
+
color: var(--brand-text);
|
|
159
|
+
text-decoration: none;
|
|
160
|
+
font-size: 10.5pt;
|
|
161
|
+
}
|
|
162
|
+
#toc ul.sectlevel0 > li > a {
|
|
163
|
+
font-weight: 600;
|
|
164
|
+
font-style: normal;
|
|
165
|
+
color: var(--brand-primary-strong);
|
|
166
|
+
}
|
|
167
|
+
#toc a::after {
|
|
168
|
+
content: leader('.') target-counter(attr(href), page);
|
|
169
|
+
color: var(--brand-text-weaker);
|
|
170
|
+
font-weight: 400;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.sect1 { break-before: page; }
|
|
174
|
+
.sect1:first-child { break-before: avoid; }
|
|
175
|
+
|
|
176
|
+
h1, h2, h3, h4, h5, h6 {
|
|
177
|
+
font-family: var(--font-body);
|
|
178
|
+
color: var(--brand-primary-strong);
|
|
179
|
+
font-weight: 700;
|
|
180
|
+
break-after: avoid;
|
|
181
|
+
}
|
|
182
|
+
h2 { font-size: 18pt; border-bottom: 1pt solid var(--brand-border); padding-bottom: 4pt; }
|
|
183
|
+
h3 { font-size: 14pt; }
|
|
184
|
+
h4 { font-size: 12pt; }
|
|
185
|
+
|
|
186
|
+
a { color: var(--brand-primary); text-decoration: none; }
|
|
187
|
+
|
|
188
|
+
/* code */
|
|
189
|
+
pre, code, kbd, tt {
|
|
190
|
+
font-family: var(--font-mono);
|
|
191
|
+
}
|
|
192
|
+
.listingblock {
|
|
193
|
+
break-inside: avoid-page;
|
|
194
|
+
margin: 1em 0;
|
|
195
|
+
}
|
|
196
|
+
.listingblock pre {
|
|
197
|
+
background: var(--brand-bg-strong);
|
|
198
|
+
border: 0.5pt solid var(--brand-border);
|
|
199
|
+
border-radius: 3pt;
|
|
200
|
+
padding: 8pt 10pt;
|
|
201
|
+
font-size: 9pt;
|
|
202
|
+
line-height: 1.4;
|
|
203
|
+
white-space: pre-wrap;
|
|
204
|
+
word-break: break-word;
|
|
205
|
+
}
|
|
206
|
+
.listingblock .title {
|
|
207
|
+
font-weight: 600;
|
|
208
|
+
color: var(--brand-text-weak);
|
|
209
|
+
font-size: 9.5pt;
|
|
210
|
+
margin-bottom: 4pt;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* tables */
|
|
214
|
+
table.tableblock {
|
|
215
|
+
border-collapse: collapse;
|
|
216
|
+
width: 100%;
|
|
217
|
+
break-inside: auto;
|
|
218
|
+
font-size: 9.5pt;
|
|
219
|
+
}
|
|
220
|
+
table.tableblock th, table.tableblock td {
|
|
221
|
+
border: 0.5pt solid var(--brand-border);
|
|
222
|
+
padding: 5pt 7pt;
|
|
223
|
+
vertical-align: top;
|
|
224
|
+
}
|
|
225
|
+
table.tableblock thead th {
|
|
226
|
+
background: var(--brand-bg-strong);
|
|
227
|
+
color: var(--brand-text);
|
|
228
|
+
font-weight: 600;
|
|
229
|
+
}
|
|
230
|
+
tr { break-inside: avoid; }
|
|
231
|
+
|
|
232
|
+
/* admonitions */
|
|
233
|
+
.admonitionblock { break-inside: avoid-page; margin: 1em 0; }
|
|
234
|
+
.admonitionblock table { width: 100%; border: none; }
|
|
235
|
+
.admonitionblock td.icon { display: none; }
|
|
236
|
+
.admonitionblock td.content {
|
|
237
|
+
border-left: 3pt solid var(--brand-border-strong);
|
|
238
|
+
padding: 6pt 10pt;
|
|
239
|
+
background: var(--brand-bg-strong);
|
|
240
|
+
}
|
|
241
|
+
.admonitionblock .title {
|
|
242
|
+
font-weight: 700;
|
|
243
|
+
text-transform: uppercase;
|
|
244
|
+
font-size: 8.5pt;
|
|
245
|
+
letter-spacing: 0.03em;
|
|
246
|
+
color: var(--brand-text-weak);
|
|
247
|
+
display: block;
|
|
248
|
+
margin-bottom: 3pt;
|
|
249
|
+
}
|
|
250
|
+
.admonitionblock.warning td.content,
|
|
251
|
+
.admonitionblock.caution td.content {
|
|
252
|
+
border-left-color: var(--brand-warning);
|
|
253
|
+
background: var(--brand-warning-bg);
|
|
254
|
+
}
|
|
255
|
+
.admonitionblock.warning .title,
|
|
256
|
+
.admonitionblock.caution .title { color: var(--brand-warning); }
|
|
257
|
+
.admonitionblock.important td.content {
|
|
258
|
+
border-left-color: var(--brand-danger);
|
|
259
|
+
background: var(--brand-danger-bg);
|
|
260
|
+
}
|
|
261
|
+
.admonitionblock.important .title { color: var(--brand-danger); }
|
|
262
|
+
.admonitionblock.note td.content,
|
|
263
|
+
.admonitionblock.tip td.content {
|
|
264
|
+
border-left-color: var(--brand-primary);
|
|
265
|
+
background: var(--brand-primary-weak);
|
|
266
|
+
}
|
|
267
|
+
.admonitionblock.note .title,
|
|
268
|
+
.admonitionblock.tip .title { color: var(--brand-primary-strong); }
|
|
269
|
+
|
|
270
|
+
/* lists */
|
|
271
|
+
.ulist, .olist, .dlist { margin: 0.5em 0; }
|
|
272
|
+
|
|
273
|
+
/* role labels (badges added by @neo4j-antora/roles-labels for label:x[] macros
|
|
274
|
+
and :page-role:/[role=label--x] roles).
|
|
275
|
+
|
|
276
|
+
This is `docs-ui/src/css/labels.css` (the *source* file the real site.css is
|
|
277
|
+
built from - github.com/neo4j-documentation/docs-ui) copied rule-for-rule,
|
|
278
|
+
minus the handful of selectors scoped under Antora's `.doc` wrapper (which
|
|
279
|
+
plain Asciidoctor HTML5 output, used for the PDF, never has - see file
|
|
280
|
+
header) - not hand-approximated values. Its `var(--label-*)`/`var(--deprecated-*)`/
|
|
281
|
+
`var(--alpha-beta-*)`/`var(--success-color)` custom properties are themselves
|
|
282
|
+
defined in `docs-ui/src/css/vars.css` in terms of raw design tokens from the
|
|
283
|
+
Needle design system (`@neo4j-ndl/base`'s `tokens/css/tokens.css`); resolved
|
|
284
|
+
to literal values below since neither of those files is otherwise pulled in
|
|
285
|
+
here. Re-resolve from those two sources if this ever drifts. */
|
|
286
|
+
.label {
|
|
287
|
+
display: inline-block;
|
|
288
|
+
/* Vertical padding trimmed from the copied 0.2rem: consecutive inline
|
|
289
|
+
labels on adjacent lines (e.g. label:x[] label:y[] each on their own
|
|
290
|
+
line) had no visible gap between them otherwise - line-height alone
|
|
291
|
+
isn't enough since the pills' padding pushed their edges flush. */
|
|
292
|
+
padding: 0.1rem 0.8rem;
|
|
293
|
+
flex-shrink: 0;
|
|
294
|
+
border-radius: 9999px;
|
|
295
|
+
background: var(--brand-primary); /* --label-default-background-color: --theme-light-color-primary-text */
|
|
296
|
+
color: var(--brand-primary-weak); /* --label-default-color: --palette-baltic-10 */
|
|
297
|
+
font-weight: 600;
|
|
298
|
+
font-size: calc(0.8 * 0.875rem); /* --label-title-font-size: calc(0.8 * --typography-label-font-size) */
|
|
299
|
+
font-style: normal;
|
|
300
|
+
}
|
|
301
|
+
.tableblock .label {
|
|
302
|
+
margin-top: 0.2rem;
|
|
303
|
+
margin-bottom: 0.5rem;
|
|
304
|
+
}
|
|
305
|
+
span.label--added,
|
|
306
|
+
span.label--changed,
|
|
307
|
+
span.label--new,
|
|
308
|
+
span.label--featured,
|
|
309
|
+
span.label--renamed,
|
|
310
|
+
span.label--updated,
|
|
311
|
+
span.label--yes {
|
|
312
|
+
background: #3f7824; /* --label-success-color: --theme-light-color-success-bg-strong */
|
|
313
|
+
color: #e7fcd7; /* --label-success-background-color: --theme-light-color-success-bg-weak */
|
|
314
|
+
}
|
|
315
|
+
span.label--admin-only,
|
|
316
|
+
span.label--danger,
|
|
317
|
+
span.label--discontinued,
|
|
318
|
+
span.label--na,
|
|
319
|
+
span.label--no,
|
|
320
|
+
span.label--not-on-aura,
|
|
321
|
+
span.label.not-available,
|
|
322
|
+
span.label--removed,
|
|
323
|
+
span.label--warning,
|
|
324
|
+
span.label--breaking {
|
|
325
|
+
background: #bb2d00; /* --label-warning-background-color: --theme-light-color-danger-bg-strong */
|
|
326
|
+
color: #ffe9e7; /* --label-warning-color: --theme-light-color-danger-bg-weak */
|
|
327
|
+
}
|
|
328
|
+
span.label--deprecated {
|
|
329
|
+
background: #ffd600; /* --deprecated-background-color: --palette-lemon-30 */
|
|
330
|
+
color: #251b00; /* --deprecated-color: --palette-lemon-80 */
|
|
331
|
+
}
|
|
332
|
+
span.label--alpha,
|
|
333
|
+
span.label--beta,
|
|
334
|
+
span.label--beta-until {
|
|
335
|
+
background: #ba7a00; /* --alpha-beta-background-color: --palette-marigold-50 */
|
|
336
|
+
color: #fff0d2; /* --alpha-beta-color: --palette-marigold-10 */
|
|
337
|
+
}
|
|
338
|
+
span.label--procedure,
|
|
339
|
+
span.label--function,
|
|
340
|
+
span.label--unix,
|
|
341
|
+
span.label--mac-os,
|
|
342
|
+
span.label--linux,
|
|
343
|
+
span.label--windows,
|
|
344
|
+
span.label--syntax,
|
|
345
|
+
span.label--functionality,
|
|
346
|
+
span.label--cypher,
|
|
347
|
+
span.label--cluster-member-core,
|
|
348
|
+
span.label--cluster-member-read-replica,
|
|
349
|
+
span.label--cluster-member-single,
|
|
350
|
+
span.label--core,
|
|
351
|
+
span.label--apoc-core,
|
|
352
|
+
span.label--full,
|
|
353
|
+
span.label--apoc-full {
|
|
354
|
+
background: #8fe3e8; /* --label-os-background-color: --theme-dark-color-primary-text */
|
|
355
|
+
color: #081e2b; /* --label-os-color: --palette-baltic-70 */
|
|
356
|
+
}
|
|
357
|
+
span.label--labs,
|
|
358
|
+
span.label--labs-label {
|
|
359
|
+
color: var(--brand-primary-weak); /* --label-labs-color: --palette-baltic-10 */
|
|
360
|
+
background: #5a34aa; /* --label-labs-background-color: --palette-lavender-45 */
|
|
361
|
+
}
|
|
362
|
+
span.label--graph-academy {
|
|
363
|
+
background: #3f7824; /* --success-color: --theme-light-color-success-bg-strong */
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
div.labels {
|
|
367
|
+
display: flex;
|
|
368
|
+
align-self: center;
|
|
369
|
+
gap: 0.25rem;
|
|
370
|
+
line-height: 1.8; /* --doc-line-height */
|
|
371
|
+
font-family: var(--font-body); /* --body-font-family */
|
|
372
|
+
}
|
|
373
|
+
.flex-labels-container {
|
|
374
|
+
display: flex;
|
|
375
|
+
justify-content: space-between;
|
|
376
|
+
align-items: flex-start;
|
|
377
|
+
flex-direction: row-reverse;
|
|
378
|
+
}
|
|
379
|
+
.header-label-container {
|
|
380
|
+
display: flex;
|
|
381
|
+
flex-wrap: wrap;
|
|
382
|
+
}
|
|
383
|
+
.admonitionblock div.labels { padding: 0.5rem 0 1rem; }
|
|
384
|
+
.exampleblock div.labels { padding: 0.5rem; }
|
|
385
|
+
.header-label-container > div.labels {
|
|
386
|
+
justify-content: space-between;
|
|
387
|
+
margin-left: auto;
|
|
388
|
+
}
|
|
389
|
+
.header-label-container > div.labels.wrapped {
|
|
390
|
+
margin-left: 0;
|
|
391
|
+
margin-top: 0.5rem;
|
|
392
|
+
}
|
|
393
|
+
h1 > .header-label {
|
|
394
|
+
margin-top: 1.2rem;
|
|
395
|
+
}
|
|
396
|
+
.header-label-container > .header-label:first-of-type {
|
|
397
|
+
margin-left: auto;
|
|
398
|
+
}
|
|
399
|
+
.listing-block .content-labels,
|
|
400
|
+
.example-block .content-labels,
|
|
401
|
+
.content-labels {
|
|
402
|
+
margin-bottom: 0.2rem;
|
|
403
|
+
}
|
|
404
|
+
.paragraph.has-label {
|
|
405
|
+
padding-left: 0.4rem;
|
|
406
|
+
border-left: 2px solid var(--brand-border-strong); /* --palette-baltic-60, not otherwise used here */
|
|
407
|
+
}
|
|
408
|
+
.paragraph.has-label:has(> .labels > .label--new) {
|
|
409
|
+
border-left-color: #3f7824; /* --success-color */
|
|
410
|
+
}
|
|
411
|
+
.paragraph.has-label:has(> .labels > .label--deprecated) {
|
|
412
|
+
border-left-color: #251b00; /* --deprecated-color */
|
|
413
|
+
}
|
|
414
|
+
h2 > .flex-label {
|
|
415
|
+
float: inline-end;
|
|
416
|
+
line-height: 1.8;
|
|
417
|
+
margin-left: 0.2rem;
|
|
418
|
+
margin-top: 0.2rem;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* TOC (hidden fixed div rendered separately by asciidoctor-web-pdf's own TOC mechanism) */
|
|
422
|
+
.toc-entry a { color: var(--brand-text); text-decoration: none; }
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Wraps the real asciidoctor-web-pdf binary as the assembler's build.command.
|
|
5
|
+
// Every path below is computed from this script's own location (__dirname) -
|
|
6
|
+
// never hardcoded, never dependent on where a consuming project's
|
|
7
|
+
// node_modules happens to hoist things - see this package's own README.
|
|
8
|
+
|
|
9
|
+
const { spawn } = require('node:child_process')
|
|
10
|
+
const path = require('node:path')
|
|
11
|
+
|
|
12
|
+
// asciidoctor-pdf needs @asciidoctor/core 4.x while Antora needs 2.x, and it
|
|
13
|
+
// doesn't declare @asciidoctor/core as its own dependency (only a peerDep on
|
|
14
|
+
// the `asciidoctor` wrapper) - npm has no signal to ever nest an isolated
|
|
15
|
+
// copy for it, so a normal dependency install can silently hoist Antora's
|
|
16
|
+
// 2.x copy in instead, which crashes at import time. `vendor/` is a real,
|
|
17
|
+
// pre-installed node_modules tree (asciidoctor + asciidoctor-pdf and their
|
|
18
|
+
// own transitive deps only) shipped as plain files with this package - not
|
|
19
|
+
// npm dependencies at all from a consumer's point of view - so it's immune
|
|
20
|
+
// to whatever else a docset's own install needs. The postprocessor/adapter
|
|
21
|
+
// extensions live inside vendor/ too, so their own `require('asciidoctor')`
|
|
22
|
+
// resolves the same isolated copy the renderer itself uses (required for
|
|
23
|
+
// `Postprocessor` - see roles-labels-postprocessor.js's own comment); their
|
|
24
|
+
// other requires (`node-html-parser`, `@neo4j-antora/roles-labels`, ...) fall
|
|
25
|
+
// through vendor/'s node_modules to the consumer's normal install, since
|
|
26
|
+
// those have no such conflict and should stay deduped normally.
|
|
27
|
+
const RENDERER = path.join(__dirname, '../vendor/node_modules/asciidoctor-pdf/bin/asciidoctor-web-pdf')
|
|
28
|
+
const STYLESHEET = path.join(__dirname, '../pdf-theme/print.css')
|
|
29
|
+
const ROLES_LABELS_POSTPROCESSOR = path.join(__dirname, '../vendor/extensions/roles-labels-postprocessor.js')
|
|
30
|
+
const TABLE_FOOTNOTES_POSTPROCESSOR = path.join(__dirname, '../vendor/extensions/table-footnotes-postprocessor.js')
|
|
31
|
+
const REMOTE_INCLUDE_ADAPTER = path.join(__dirname, '../vendor/extensions/remote-include-adapter.js')
|
|
32
|
+
const MACROS_ADAPTER = path.join(__dirname, '../vendor/extensions/macros-adapter.js')
|
|
33
|
+
|
|
34
|
+
const PAGE_BOUNDARY_RX = /(?=^:page-docname: .*$)/m
|
|
35
|
+
const GLOSSARY_MARKER_RX = /^\[discrete\.glossary#.*\]$/m
|
|
36
|
+
|
|
37
|
+
// Some docsets (e.g. docs-http-api) repeat a glossary include on several
|
|
38
|
+
// source pages, meant to be excluded from PDF via ifndef::backend-pdf[],
|
|
39
|
+
// which doesn't evaluate correctly under this pipeline (see this package's
|
|
40
|
+
// own README - the assembler resolves that attribute against its own
|
|
41
|
+
// internal re-parse context, not the real, final PDF conversion). This is a
|
|
42
|
+
// no-op for any docset with no `[discrete.glossary#...]` marker in its
|
|
43
|
+
// merged source.
|
|
44
|
+
function dedupeGlossary (adoc) {
|
|
45
|
+
const [preamble, ...pages] = adoc.split(PAGE_BOUNDARY_RX)
|
|
46
|
+
let glossaryChunk
|
|
47
|
+
const strippedPages = pages.map((page) => {
|
|
48
|
+
const match = GLOSSARY_MARKER_RX.exec(page)
|
|
49
|
+
if (!match) return page
|
|
50
|
+
if (!glossaryChunk) {
|
|
51
|
+
// Drop the "discrete" style so the glossary becomes a normal chapter
|
|
52
|
+
// section: included in the TOC and picked up by the print theme's
|
|
53
|
+
// `.sect1 { break-before: page }` rule like every other chapter.
|
|
54
|
+
glossaryChunk = page
|
|
55
|
+
.slice(match.index)
|
|
56
|
+
.trimEnd()
|
|
57
|
+
.replace(/^\[discrete\.glossary/, '[glossary')
|
|
58
|
+
}
|
|
59
|
+
// Keep a blank line before whatever follows (the next page's own
|
|
60
|
+
// `:page-docname:` metadata block, mid-document) - an undelimited
|
|
61
|
+
// single-paragraph admonition like [NOTE] only ends at a blank line, so
|
|
62
|
+
// trimming it away merges the next page's raw attribute lines straight
|
|
63
|
+
// into that paragraph's text instead of stopping it.
|
|
64
|
+
return page.slice(0, match.index).trimEnd() + '\n\n'
|
|
65
|
+
})
|
|
66
|
+
let result = preamble + strippedPages.join('')
|
|
67
|
+
if (glossaryChunk) result = result.trimEnd() + '\n\n' + glossaryChunk + '\n'
|
|
68
|
+
return result
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readStdin () {
|
|
72
|
+
const chunks = []
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
process.stdin.on('data', (chunk) => chunks.push(chunk))
|
|
75
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
76
|
+
process.stdin.on('error', reject)
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// asciidoctor-web-pdf only reads these from the environment (no -a attribute or
|
|
81
|
+
// CLI flag equivalent - see lib/browser.js) and defaults to 30s, which a large
|
|
82
|
+
// docset can easily exceed. Give it more headroom here rather than relying on
|
|
83
|
+
// whoever runs the build to remember to export these.
|
|
84
|
+
const PUPPETEER_TIMEOUT_ENV = {
|
|
85
|
+
PUPPETEER_NAVIGATION_TIMEOUT: '180000',
|
|
86
|
+
PUPPETEER_RENDERING_TIMEOUT: '180000',
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// antora-assembler-pdf.yml deliberately does NOT set a `stylesheet` attribute
|
|
90
|
+
// itself - it's added here instead, __dirname-computed, so the path is always
|
|
91
|
+
// correct wherever this package happens to be installed. CSS url()s resolve
|
|
92
|
+
// relative to the stylesheet's own location, but Asciidoctor's `stylesheet`
|
|
93
|
+
// attribute resolves relative to docdir/cwd - a different base entirely - so
|
|
94
|
+
// this can't just be a relative value in the playbook/assembler config.
|
|
95
|
+
const args = process.argv.slice(2)
|
|
96
|
+
const stdinMarkerIdx = args.lastIndexOf('-')
|
|
97
|
+
// roles-labels-postprocessor.js and table-footnotes-postprocessor.js port the
|
|
98
|
+
// essential parts of the Antora extensions of the same name (see those
|
|
99
|
+
// files); macros-adapter.js and remote-include-adapter.js wrap the real
|
|
100
|
+
// @neo4j-documentation packages - added here, __dirname-computed, for the
|
|
101
|
+
// same reason as the stylesheet above.
|
|
102
|
+
const extraArgs = [
|
|
103
|
+
'-a', `stylesheet=${STYLESHEET}`,
|
|
104
|
+
'--extension', ROLES_LABELS_POSTPROCESSOR,
|
|
105
|
+
'--extension', TABLE_FOOTNOTES_POSTPROCESSOR,
|
|
106
|
+
'--extension', REMOTE_INCLUDE_ADAPTER,
|
|
107
|
+
'--extension', MACROS_ADAPTER,
|
|
108
|
+
]
|
|
109
|
+
const finalArgs =
|
|
110
|
+
stdinMarkerIdx === -1
|
|
111
|
+
? [...args, ...extraArgs]
|
|
112
|
+
: [...args.slice(0, stdinMarkerIdx), ...extraArgs, ...args.slice(stdinMarkerIdx)]
|
|
113
|
+
|
|
114
|
+
readStdin().then((adoc) => {
|
|
115
|
+
const child = spawn(RENDERER, finalArgs, {
|
|
116
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
117
|
+
env: { ...PUPPETEER_TIMEOUT_ENV, ...process.env },
|
|
118
|
+
})
|
|
119
|
+
child.on('error', (err) => {
|
|
120
|
+
console.error(err)
|
|
121
|
+
process.exit(1)
|
|
122
|
+
})
|
|
123
|
+
child.on('close', (status) => process.exit(status ?? 1))
|
|
124
|
+
child.stdin.end(dedupeGlossary(adoc))
|
|
125
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Runs as this package's own `postinstall` (see package.json) - installs
|
|
5
|
+
// asciidoctor-pdf + the exact Asciidoctor.js it needs (4.x) into vendor/,
|
|
6
|
+
// fresh, in whichever environment this package itself just got installed
|
|
7
|
+
// into. asciidoctor-pdf needs @asciidoctor/core 4.x while Antora needs 2.x,
|
|
8
|
+
// and asciidoctor-pdf doesn't declare @asciidoctor/core as its own
|
|
9
|
+
// dependency (only a peerDep on the `asciidoctor` wrapper) - npm has no
|
|
10
|
+
// signal to isolate it from a docset's own shared install, and can silently
|
|
11
|
+
// hoist Antora's incompatible 2.x copy in instead, which crashes at import
|
|
12
|
+
// time. A real, separate `npm install` in vendor/'s own directory - exactly
|
|
13
|
+
// what the old isolated `renderer/` project this replaces did by hand -
|
|
14
|
+
// fixes that; running it from this package's own `postinstall` makes it
|
|
15
|
+
// automatic instead of a manual convention a consumer has to know about.
|
|
16
|
+
//
|
|
17
|
+
// vendor/package.json is written here, at install time, rather than shipped
|
|
18
|
+
// as a static file in the published package: a *shipped* nested
|
|
19
|
+
// package.json triggers some npm install-time reorganization that quietly
|
|
20
|
+
// merges vendor/'s own subfolders into this package's root and drops the
|
|
21
|
+
// file entirely (confirmed directly, by inspecting exactly what a real `npm
|
|
22
|
+
// install` of a real packed tarball produces vs. what plain `tar -x` of that
|
|
23
|
+
// same tarball contains - the tarball itself was correct, so this is npm's
|
|
24
|
+
// install step specifically, not a packaging mistake). Writing it fresh here
|
|
25
|
+
// avoids whatever triggers that.
|
|
26
|
+
//
|
|
27
|
+
// A published package's own devDependencies (used for local development -
|
|
28
|
+
// see this package's package.json) are never installed for a consumer at
|
|
29
|
+
// all, so this can't just be `asciidoctor`/`asciidoctor-pdf` deps declared
|
|
30
|
+
// there instead - nothing would ever install them for anyone but a
|
|
31
|
+
// contributor working on this package directly.
|
|
32
|
+
|
|
33
|
+
const { spawnSync } = require('node:child_process')
|
|
34
|
+
const fs = require('node:fs')
|
|
35
|
+
const path = require('node:path')
|
|
36
|
+
|
|
37
|
+
const vendorDir = path.join(__dirname, '../vendor')
|
|
38
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
39
|
+
|
|
40
|
+
fs.writeFileSync(
|
|
41
|
+
path.join(vendorDir, 'package.json'),
|
|
42
|
+
JSON.stringify(
|
|
43
|
+
{
|
|
44
|
+
name: 'pdf-generator-vendor',
|
|
45
|
+
version: '1.0.0',
|
|
46
|
+
private: true,
|
|
47
|
+
dependencies: {
|
|
48
|
+
asciidoctor: '4.1.0',
|
|
49
|
+
'asciidoctor-pdf': '1.0.2',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
null,
|
|
53
|
+
2
|
|
54
|
+
) + '\n'
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const result = spawnSync(npmCmd, ['install', '--omit=dev', '--no-audit', '--no-fund'], {
|
|
58
|
+
cwd: vendorDir,
|
|
59
|
+
stdio: 'inherit',
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
if (result.error || result.status !== 0) {
|
|
63
|
+
console.error('@neo4j-antora/pdf-generator: failed to install its isolated asciidoctor-pdf renderer (see vendor/package.json)')
|
|
64
|
+
process.exit(result.status || 1)
|
|
65
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// @neo4j-documentation/macros' `label:x[]` inline macro reads
|
|
4
|
+
// `attr.$positional` (pre-4.x Asciidoctor.js's Opal-bridge attribute
|
|
5
|
+
// wrapper) to get the macro's positional text argument - removed in
|
|
6
|
+
// Asciidoctor.js 4.x, which instead hands a plain object with 1-indexed
|
|
7
|
+
// string keys (e.g. `{"1": "Custom text"}`). Without a fix, `attr.$positional`
|
|
8
|
+
// is just `undefined`, so any custom inline label text (e.g.
|
|
9
|
+
// `label:new[Custom text]`) is silently dropped in favour of the default
|
|
10
|
+
// display text - it doesn't throw, so this is easy to miss.
|
|
11
|
+
//
|
|
12
|
+
// Rather than patch the vendored file with patch-package (its postinstall
|
|
13
|
+
// hook isn't guaranteed to run once this package is itself a nested
|
|
14
|
+
// dependency of a docset's install - npm doesn't run a dependency's own
|
|
15
|
+
// lifecycle scripts by default), this wraps `registry.inlineMacro` so any
|
|
16
|
+
// attrs object handed to a macro's `process` callback gets `$positional`
|
|
17
|
+
// backfilled from those numeric keys, matching the shape macros.js expects.
|
|
18
|
+
//
|
|
19
|
+
// Must intercept via a Proxy rather than binding/replacing `self.process`
|
|
20
|
+
// directly - the installed Asciidoctor.js engine here is a compiled/WASM
|
|
21
|
+
// binding, and a `.bind()`'d reference to its native `process` method
|
|
22
|
+
// silently never invokes the callback it's given, even though calling it
|
|
23
|
+
// unbound (`target.process(fn)`) works correctly. Verified directly against
|
|
24
|
+
// the unpatched package and the installed engine.
|
|
25
|
+
const macros = require('@neo4j-documentation/macros')
|
|
26
|
+
|
|
27
|
+
function backfillPositionalAttrs (attrs) {
|
|
28
|
+
if (!attrs || attrs.$positional) return
|
|
29
|
+
const positional = []
|
|
30
|
+
let i = 1
|
|
31
|
+
while (attrs[String(i)] !== undefined) {
|
|
32
|
+
positional.push(attrs[String(i)])
|
|
33
|
+
i++
|
|
34
|
+
}
|
|
35
|
+
if (positional.length) attrs.$positional = positional
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function shimOpalPositionalAttrs (registry) {
|
|
39
|
+
const original = registry.inlineMacro.bind(registry)
|
|
40
|
+
registry.inlineMacro = function (name, fn) {
|
|
41
|
+
return original(name, function (...outerArgs) {
|
|
42
|
+
const target = this
|
|
43
|
+
const selfProxy = new Proxy(target, {
|
|
44
|
+
get (t, prop, receiver) {
|
|
45
|
+
if (prop === 'process') {
|
|
46
|
+
return function (processFn) {
|
|
47
|
+
return t.process(function (parent, macroTarget, attrs) {
|
|
48
|
+
backfillPositionalAttrs(attrs)
|
|
49
|
+
return processFn(parent, macroTarget, attrs)
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return Reflect.get(t, prop, receiver)
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
return fn.apply(selfProxy, outerArgs)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports.register = function (registry, context) {
|
|
62
|
+
shimOpalPositionalAttrs(registry)
|
|
63
|
+
macros.register(registry, context)
|
|
64
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// @neo4j-documentation/remote-include exports its registration function
|
|
4
|
+
// directly (`module.exports = function () { this.includeProcessor(...) }`),
|
|
5
|
+
// which is what Antora's own extension loader expects, but not what
|
|
6
|
+
// asciidoctor-web-pdf's --extension loader does (it requires `lib.register`
|
|
7
|
+
// to be a function - see requireLibrary/_prepareExtensions in
|
|
8
|
+
// asciidoctor/lib/cli.js). This bridges the two conventions.
|
|
9
|
+
//
|
|
10
|
+
// It also works around a second, unrelated problem: the package's own
|
|
11
|
+
// includeProcessor body calls the pre-4.x Asciidoctor.js Opal-bridge API
|
|
12
|
+
// (`this.$option(...)`), removed in 4.x (now `this.option(...)`) - see
|
|
13
|
+
// macros-adapter.js for the same class of bug in a different package.
|
|
14
|
+
// Rather than patch the vendored file with patch-package (its postinstall
|
|
15
|
+
// hook isn't guaranteed to run once this package is itself a nested
|
|
16
|
+
// dependency of a docset's install - npm doesn't run a dependency's own
|
|
17
|
+
// lifecycle scripts by default), this wraps `registry.includeProcessor`
|
|
18
|
+
// so the callback's `this` transparently answers `$option` calls by
|
|
19
|
+
// forwarding to the real `option` method. Verified directly against the
|
|
20
|
+
// unpatched package and the installed Asciidoctor.js engine.
|
|
21
|
+
|
|
22
|
+
const remoteInclude = require('@neo4j-documentation/remote-include')
|
|
23
|
+
|
|
24
|
+
function shimOpalOptionApi (registry) {
|
|
25
|
+
const original = registry.includeProcessor.bind(registry)
|
|
26
|
+
registry.includeProcessor = function (fn) {
|
|
27
|
+
return original(function (...args) {
|
|
28
|
+
const target = this
|
|
29
|
+
const self = new Proxy(target, {
|
|
30
|
+
get (t, prop, receiver) {
|
|
31
|
+
if (prop === '$option') return t.option.bind(t)
|
|
32
|
+
return Reflect.get(t, prop, receiver)
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
return fn.apply(self, args)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports.register = function (registry) {
|
|
41
|
+
shimOpalOptionApi(registry)
|
|
42
|
+
remoteInclude.call(registry)
|
|
43
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Reuses @neo4j-antora/roles-labels' own shared core (extensions/antora/
|
|
4
|
+
// roles-labels/lib/process-labels.js in docs-tools) so PDF labels are
|
|
5
|
+
// produced by the exact same logic as the real HTML site - synonym
|
|
6
|
+
// resolution, version/product-suffix stripping, dataset attributes, inline
|
|
7
|
+
// vs role handling included - rather than a hand-ported subset that can
|
|
8
|
+
// silently drift from it (see docs-tools/pdf/README).
|
|
9
|
+
//
|
|
10
|
+
// roles-labels itself can't be *registered* here - it hooks Antora's own
|
|
11
|
+
// `pagesComposed` event and operates on files in an Antora ContentCatalog,
|
|
12
|
+
// which never exist in this pipeline: asciidoctor-web-pdf runs a completely
|
|
13
|
+
// separate, isolated Asciidoctor conversion on the assembler's merged .adoc
|
|
14
|
+
// text, so roles-labels never gets a chance to see - let alone transform -
|
|
15
|
+
// this content. This is a Postprocessor instead (Asciidoctor's own extension
|
|
16
|
+
// point, run after conversion to HTML, before the PDF renderer sees it) that
|
|
17
|
+
// calls the same shared `processLabels` function roles-labels.js calls.
|
|
18
|
+
|
|
19
|
+
const { parse: parseHTML } = require('node-html-parser')
|
|
20
|
+
// Must come from the same package identity the running engine (loaded via
|
|
21
|
+
// `asciidoctor`, not `@asciidoctor/core` directly, by asciidoctor-web-pdf/
|
|
22
|
+
// the CLI) uses internally - requiring the class from a different copy of
|
|
23
|
+
// the module fails Registry's own instanceof-style check with "Invalid type
|
|
24
|
+
// for postprocessor extension" even though it's structurally identical.
|
|
25
|
+
const { Postprocessor } = require('asciidoctor')
|
|
26
|
+
const { processLabels } = require('@neo4j-antora/roles-labels/lib/process-labels')
|
|
27
|
+
|
|
28
|
+
// Minimal shim matching the { info(meta, msg, ...args), warn(...), debug(...) }
|
|
29
|
+
// shape processLabels expects from Antora's pino-based logger - this pipeline
|
|
30
|
+
// has no Antora logger to reuse.
|
|
31
|
+
function makeLogger () {
|
|
32
|
+
const log = (level) => (meta, msg, ...args) => {
|
|
33
|
+
const consoleMethod = level === 'debug' ? 'log' : level
|
|
34
|
+
// eslint-disable-next-line no-console
|
|
35
|
+
console[consoleMethod](`[roles-labels] ${msg}`.replace(/%s/g, () => args.shift()), meta)
|
|
36
|
+
}
|
|
37
|
+
return { info: log('info'), warn: log('warn'), error: log('error'), debug: log('debug') }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const logger = makeLogger()
|
|
41
|
+
|
|
42
|
+
class RolesLabelsPostprocessor extends Postprocessor {
|
|
43
|
+
process (document, output) {
|
|
44
|
+
if (!output.includes('label--')) return output
|
|
45
|
+
const root = parseHTML(output)
|
|
46
|
+
processLabels(root, {
|
|
47
|
+
src: { path: 'pdf-export' },
|
|
48
|
+
attributes: document.getAttributes(),
|
|
49
|
+
logger,
|
|
50
|
+
defaultLogLevel: 'info',
|
|
51
|
+
replaceInlineLabelText: false,
|
|
52
|
+
// Plain Asciidoctor HTML5 output (used here) never has Antora's
|
|
53
|
+
// `article.doc` wrapper - falls back to the parsed root itself, since
|
|
54
|
+
// there's no better place to hang page-wide dataset attributes.
|
|
55
|
+
docRootSelector: 'article.doc',
|
|
56
|
+
// @antora/assembler marks *every* merged page's heading `discrete` to
|
|
57
|
+
// flatten section nesting/IDs across the book - unlike on a real
|
|
58
|
+
// Antora page, that's never a signal the author marked this heading as
|
|
59
|
+
// a non-section (see process-labels.js's own comment on this option),
|
|
60
|
+
// so a role on it must still become a label, exactly as it would in
|
|
61
|
+
// the HTML this page was built from.
|
|
62
|
+
skipDiscrete: false,
|
|
63
|
+
})
|
|
64
|
+
return root.toString()
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports.register = function (registry) {
|
|
69
|
+
registry.postprocessor(RolesLabelsPostprocessor)
|
|
70
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Ports @neo4j-antora/table-footnotes for the PDF pipeline: moving a table's
|
|
4
|
+
// own footnotes out of Asciidoctor's single document-wide #footnotes div and
|
|
5
|
+
// into a <tfoot> row on that specific table, instead of leaving them
|
|
6
|
+
// dumped at the very end of the whole document, disconnected from the table
|
|
7
|
+
// they came from.
|
|
8
|
+
//
|
|
9
|
+
// Same reason this can't just reuse table-footnotes.js directly as roles-
|
|
10
|
+
// labels-postprocessor.js: it hooks Antora's own `pagesComposed` event and
|
|
11
|
+
// operates on files in an Antora ContentCatalog, neither of which exist in
|
|
12
|
+
// this pipeline (see docs-tools/pdf/README) - asciidoctor-web-pdf runs a
|
|
13
|
+
// separate, isolated Asciidoctor conversion on the assembler's merged .adoc
|
|
14
|
+
// text that table-footnotes never gets a chance to see. This is a
|
|
15
|
+
// Postprocessor instead (Asciidoctor's own extension point, run after
|
|
16
|
+
// conversion to HTML), with the same DOM manipulation ported over near
|
|
17
|
+
// verbatim - it's pure HTML restructuring, no Antora-specific data needed.
|
|
18
|
+
|
|
19
|
+
const { parse: parseHTML } = require('node-html-parser')
|
|
20
|
+
// Must come from the same package identity the running engine uses
|
|
21
|
+
// internally, not a separately-resolved copy - see roles-labels-
|
|
22
|
+
// postprocessor.js for why requiring the class from the wrong copy of the
|
|
23
|
+
// module fails Registry's own type check.
|
|
24
|
+
const { Postprocessor } = require('asciidoctor')
|
|
25
|
+
|
|
26
|
+
function createElement (el, className = '') {
|
|
27
|
+
return parseHTML(`<${el}${className ? ` class="${className}"` : ''}></${el}>`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class TableFootnotesPostprocessor extends Postprocessor {
|
|
31
|
+
process (_document, output) {
|
|
32
|
+
if (!output.includes('id="footnotes"')) return output
|
|
33
|
+
const root = parseHTML(output)
|
|
34
|
+
const footnotesDiv = root.getElementById('footnotes')
|
|
35
|
+
const tables = root.querySelectorAll('table')
|
|
36
|
+
if (!footnotesDiv || tables.length === 0) return output
|
|
37
|
+
|
|
38
|
+
tables.forEach((table) => {
|
|
39
|
+
const tableFootnotes = table.querySelectorAll('tbody a.footnote')
|
|
40
|
+
if (tableFootnotes.length === 0) return
|
|
41
|
+
|
|
42
|
+
const cols = table.querySelectorAll('colgroup col').length
|
|
43
|
+
const tFoot = createElement('tfoot')
|
|
44
|
+
const footnoteRow = createElement('tr')
|
|
45
|
+
tFoot.firstElementChild.appendChild(footnoteRow)
|
|
46
|
+
const footnoteCell = createElement('td', 'tableblock footnote-cell')
|
|
47
|
+
footnoteCell.firstElementChild.setAttribute('colspan', cols)
|
|
48
|
+
|
|
49
|
+
// For each footnote reference in this table, find the matching
|
|
50
|
+
// footnote definition (by id, from its href) in the document-wide
|
|
51
|
+
// footnotes div, and move it into this table's own footer.
|
|
52
|
+
tableFootnotes.forEach((footnote) => {
|
|
53
|
+
const footnoteId = footnote.getAttribute('href').replace('#', '')
|
|
54
|
+
const matchingFootnote = footnotesDiv.querySelector(`#${footnoteId}`)
|
|
55
|
+
if (!matchingFootnote) return
|
|
56
|
+
footnoteCell.firstElementChild.appendChild(matchingFootnote)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
footnoteRow.firstElementChild.appendChild(footnoteCell)
|
|
60
|
+
table.appendChild(tFoot)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Remove the document-wide footnotes div if every footnote in it ended
|
|
64
|
+
// up moved into a table footer.
|
|
65
|
+
if (footnotesDiv.querySelectorAll('div.footnote').length === 0) {
|
|
66
|
+
footnotesDiv.remove()
|
|
67
|
+
}
|
|
68
|
+
return root.toString()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports.register = function (registry) {
|
|
73
|
+
registry.postprocessor(TableFootnotesPostprocessor)
|
|
74
|
+
}
|