@asciidoctor/core 3.0.4 → 4.0.0-alpha.2
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.md +42 -10
- package/build/browser/index.js +24156 -0
- package/build/node/index.cjs +24737 -0
- package/{dist/css/asciidoctor.css → data/asciidoctor-default.css} +54 -53
- package/package.json +55 -97
- package/src/abstract_block.js +857 -0
- package/src/abstract_node.js +954 -0
- package/src/attribute_entry.js +12 -0
- package/src/attribute_list.js +380 -0
- package/src/block.js +168 -0
- package/src/browser/asset.js +22 -0
- package/src/browser/reader.js +138 -0
- package/src/browser.js +121 -0
- package/src/callouts.js +85 -0
- package/src/compliance.js +54 -0
- package/src/constants.js +665 -0
- package/src/convert.js +370 -0
- package/src/converter/composite.js +83 -0
- package/src/converter/docbook5.js +1031 -0
- package/src/converter/html5.js +1893 -0
- package/src/converter/manpage.js +935 -0
- package/src/converter/template.js +459 -0
- package/src/converter.js +478 -0
- package/src/data/stylesheet-data.js +2 -0
- package/src/document.js +2134 -0
- package/src/extensions.js +1952 -0
- package/src/footnote.js +28 -0
- package/src/helpers.js +355 -0
- package/src/index.js +138 -0
- package/src/inline.js +158 -0
- package/src/list.js +240 -0
- package/src/load.js +276 -0
- package/src/logging.js +526 -0
- package/src/parser.js +3661 -0
- package/src/path_resolver.js +472 -0
- package/src/reader.js +1755 -0
- package/src/rx.js +829 -0
- package/src/section.js +354 -0
- package/src/stylesheets.js +30 -0
- package/src/substitutors.js +2241 -0
- package/src/syntaxHighlighter/highlightjs.js +90 -0
- package/src/syntaxHighlighter/html_pipeline.js +33 -0
- package/src/syntax_highlighter.js +304 -0
- package/src/table.js +952 -0
- package/src/timings.js +78 -0
- package/types/abstract_block.d.ts +346 -0
- package/types/abstract_node.d.ts +471 -0
- package/types/attribute_entry.d.ts +7 -0
- package/types/attribute_list.d.ts +52 -0
- package/types/block.d.ts +55 -0
- package/types/browser/asset.d.ts +7 -0
- package/types/browser/reader.d.ts +29 -0
- package/types/callouts.d.ts +36 -0
- package/types/compliance.d.ts +23 -0
- package/types/constants.d.ts +268 -0
- package/types/convert.d.ts +34 -0
- package/types/converter/composite.d.ts +20 -0
- package/types/converter/docbook5.d.ts +41 -0
- package/types/converter/html5.d.ts +51 -0
- package/types/converter/manpage.d.ts +59 -0
- package/types/converter/template.d.ts +83 -0
- package/types/converter.d.ts +150 -0
- package/types/data/stylesheet-data.d.ts +2 -0
- package/types/document.d.ts +495 -0
- package/types/extensions.d.ts +876 -0
- package/types/footnote.d.ts +18 -0
- package/types/helpers.d.ts +146 -0
- package/types/index.d.cts +75 -0
- package/types/index.d.ts +73 -3731
- package/types/inline.d.ts +69 -0
- package/types/list.d.ts +114 -0
- package/types/load.d.ts +39 -0
- package/types/logging.d.ts +187 -0
- package/types/parser.d.ts +114 -0
- package/types/path_resolver.d.ts +103 -0
- package/types/reader.d.ts +184 -0
- package/types/rx.d.ts +513 -0
- package/types/section.d.ts +122 -0
- package/types/stylesheets.d.ts +10 -0
- package/types/substitutors.d.ts +208 -0
- package/types/syntaxHighlighter/highlightjs.d.ts +33 -0
- package/types/syntaxHighlighter/html_pipeline.d.ts +16 -0
- package/types/syntax_highlighter.d.ts +167 -0
- package/types/table.d.ts +231 -0
- package/types/timings.d.ts +25 -0
- package/types/tsconfig.json +9 -0
- package/LICENSE +0 -21
- package/dist/browser/asciidoctor.js +0 -47654
- package/dist/browser/asciidoctor.min.js +0 -1452
- package/dist/graalvm/asciidoctor.js +0 -47402
- package/dist/node/asciidoctor.cjs +0 -21567
- package/dist/node/asciidoctor.js +0 -23037
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
// ESM conversion of converter/manpage.rb
|
|
2
|
+
//
|
|
3
|
+
// Ruby-to-JavaScript notes:
|
|
4
|
+
// - Ruby module constants (WHITESPACE, ESC, …) → module-level const
|
|
5
|
+
// - Ruby symbol keys (:preserve, :normalize, :collapse) → plain strings
|
|
6
|
+
// - node.attr? → node.hasAttribute()
|
|
7
|
+
// - node.title? → node.hasTitle()
|
|
8
|
+
// - node.blocks? → node.hasBlocks()
|
|
9
|
+
// - node.footnotes? → node.hasFootnotes()
|
|
10
|
+
// - node.noheader → node.isNoheader()
|
|
11
|
+
// - node.authors → node.authors() (method call)
|
|
12
|
+
// - node.footnotes → node.footnotes (getter)
|
|
13
|
+
// - await node.content() → await node.content() (method call)
|
|
14
|
+
// - node.text → node.text (property/getter)
|
|
15
|
+
// - node.captioned_title → node.captionedTitle()
|
|
16
|
+
// - node.content_model == :compound → node.contentModel === 'compound'
|
|
17
|
+
// - node.rows.to_h.each { |tsec, rows| } → for (const [tsec, rows] of node.rows.bySection())
|
|
18
|
+
// - node.media_uri target → node.mediaUri(target)
|
|
19
|
+
// - AbstractNode === ref → ref instanceof AbstractNode
|
|
20
|
+
// - node.context === :section → node.context === 'section'
|
|
21
|
+
// - node.document.catalog[:refs] → node.document.catalog.refs
|
|
22
|
+
// - Ruby gsub blocks with $1, $2 → replace callbacks with (m, $1, $2, ...)
|
|
23
|
+
// - Ruby str.tr_s(WHITESPACE, ' ') → str.replace(/[\n\t ]+/g, ' ')
|
|
24
|
+
// - Ruby str.rstrip → str.trimEnd()
|
|
25
|
+
// - Ruby str.lstrip → str.trimStart()
|
|
26
|
+
// - self.write_alternate_pages → static writeAlternatePages; uses lazy node:fs import
|
|
27
|
+
// - (^)? capture of zero-width anchor: Ruby empty string is truthy, JS empty string is falsy
|
|
28
|
+
// → use ($1 !== undefined) instead of ($1) in preserve-whitespace handler
|
|
29
|
+
|
|
30
|
+
import { ConverterBase } from '../converter.js'
|
|
31
|
+
import { AbstractNode } from '../abstract_node.js'
|
|
32
|
+
import { LF, TAB, BLOCK_MATH_DELIMITERS } from '../constants.js'
|
|
33
|
+
import { InvalidSectionIdCharsRx } from '../rx.js'
|
|
34
|
+
|
|
35
|
+
// ── Module-level constants ────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const _WHITESPACE = `${LF}${TAB} ` // "\n\t "
|
|
38
|
+
const ET = ' '.repeat(8) // expand tab to 8 spaces
|
|
39
|
+
const ESC = '\u001b' // troff leader marker
|
|
40
|
+
const ESC_BS = `${ESC}\\` // escaped backslash (troff formatting sequence)
|
|
41
|
+
const ESC_FS = `${ESC}.` // escaped full stop (troff macro)
|
|
42
|
+
|
|
43
|
+
// ── Module-level regular expressions ─────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
// Matches a literal backslash at string start (^\\) OR an optionally ESC-prefixed backslash
|
|
46
|
+
// Replacement rule: if ESC-prefixed ($1 set) → keep as-is; otherwise → \\(rs
|
|
47
|
+
const LiteralBackslashRx = /^\\|(\u001b)?\\/g
|
|
48
|
+
|
|
49
|
+
// Matches a leading period on any line (troff macro indicator)
|
|
50
|
+
const LeadingPeriodRx = /^\./gm
|
|
51
|
+
|
|
52
|
+
// Matches a full escaped URL/MTO macro line (possibly prefixed by orphaned \c line)
|
|
53
|
+
const EscapedMacroRx =
|
|
54
|
+
/^(?:\u001b\\c\n)?\u001b\.((?:URL|MTO) ".*?" ".*?" )( |[^\s]*)(.*?)(?: *\u001b\\c)?$/gm
|
|
55
|
+
|
|
56
|
+
// Matches malformed escaped macros (orphaned \c followed by ESC macro without newline)
|
|
57
|
+
const MalformedEscapedMacroRx = /(\u001b\\c) (\u001b\.(?:URL|MTO) )/g
|
|
58
|
+
|
|
59
|
+
// Matches mock XML boundary markers used to avoid artificial word-breaks
|
|
60
|
+
const MockMacroRx = /<\/?([\u001b]\\[^>]+)>/g
|
|
61
|
+
|
|
62
|
+
// HTML entity references for em-dash and ellipsis
|
|
63
|
+
const EmDashCharRefRx = /—(?:​)?/g
|
|
64
|
+
const EllipsisCharRefRx = /…(?:​)?/g
|
|
65
|
+
|
|
66
|
+
// Whitespace normalisation: optional blanks around a newline → single newline
|
|
67
|
+
const WrappedIndentRx = /[ \t]*\n[ \t]*/g
|
|
68
|
+
|
|
69
|
+
// Detects any XML/entity markup in a string (used by uppercase_pcdata)
|
|
70
|
+
const XMLMarkupRx = /&#?[a-z\d]+;|</
|
|
71
|
+
|
|
72
|
+
// Splits a string into entity refs / fake-XML spans / monospaced spans / plain text
|
|
73
|
+
const PCDATAFilterRx =
|
|
74
|
+
/(&#?[a-z\d]+;|<\u001b\\f\(CR[\s\S]*?<\/\u001b\\fP>|<[^>]+>)|([^&<]+)/g
|
|
75
|
+
|
|
76
|
+
// ── ManPageConverter ──────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export default class ManPageConverter extends ConverterBase {
|
|
79
|
+
constructor(backend, opts = {}) {
|
|
80
|
+
super(backend, opts)
|
|
81
|
+
this.initBackendTraits({
|
|
82
|
+
basebackend: 'manpage',
|
|
83
|
+
filetype: 'man',
|
|
84
|
+
outfilesuffix: '.man',
|
|
85
|
+
supportsTemplates: true,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async convert_document(node) {
|
|
90
|
+
if (!node.hasAttribute('mantitle')) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
'asciidoctor: ERROR: doctype must be set to manpage when using manpage backend'
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
const mantitle = node
|
|
96
|
+
.getAttribute('mantitle')
|
|
97
|
+
.replace(InvalidSectionIdCharsRx, '')
|
|
98
|
+
const manvolnum = node.getAttribute('manvolnum', '1')
|
|
99
|
+
const manname = node.getAttribute('manname', mantitle)
|
|
100
|
+
const manmanual = node.getAttribute('manmanual')
|
|
101
|
+
const mansource = node.getAttribute('mansource')
|
|
102
|
+
const docdate = node.hasAttribute('reproducible')
|
|
103
|
+
? null
|
|
104
|
+
: node.getAttribute('docdate')
|
|
105
|
+
|
|
106
|
+
// NOTE the first line enables the table (tbl) preprocessor, necessary for non-Linux systems
|
|
107
|
+
const result = [
|
|
108
|
+
`'\\" t
|
|
109
|
+
.\\" Title: ${mantitle}
|
|
110
|
+
.\\" Author: ${node.hasAttribute('authors') ? node.getAttribute('authors') : '[see the "AUTHOR(S)" section]'}
|
|
111
|
+
.\\" Generator: Asciidoctor ${node.getAttribute('asciidoctor-version')}`,
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
if (docdate) result.push(`.\\" Date: ${docdate}`)
|
|
115
|
+
|
|
116
|
+
result.push(`.\\" Manual: ${manmanual ? manmanual.replace(/[\n\t ]+/g, ' ') : '\\ \\&'}
|
|
117
|
+
.\\" Source: ${mansource ? mansource.replace(/[\n\t ]+/g, ' ') : '\\ \\&'}
|
|
118
|
+
.\\" Language: English
|
|
119
|
+
.\\"`)
|
|
120
|
+
|
|
121
|
+
// TODO add document-level setting to disable capitalization of manname
|
|
122
|
+
result.push(
|
|
123
|
+
`.TH "${this.manify(manname.toUpperCase())}" "${manvolnum}" "${docdate ?? ''}" "${mansource ? this.manify(mansource) : '\\ \\&'}" "${manmanual ? this.manify(manmanual) : '\\ \\&'}"`
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
// define portability settings
|
|
127
|
+
// see http://bugs.debian.org/507673
|
|
128
|
+
// see http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
|
|
129
|
+
result.push('.ie \\n(.g .ds Aq \\(aq')
|
|
130
|
+
result.push(".el .ds Aq '")
|
|
131
|
+
// set sentence_space_size to 0 to prevent extra space between sentences separated by a newline
|
|
132
|
+
result.push('.ss \\n[.ss] 0')
|
|
133
|
+
// disable hyphenation
|
|
134
|
+
result.push('.nh')
|
|
135
|
+
// disable justification (adjust text to left margin only)
|
|
136
|
+
result.push('.ad l')
|
|
137
|
+
// define URL macro for portability
|
|
138
|
+
// see http://web.archive.org/web/20060102165607/http://people.debian.org/~branden/talks/wtfm/wtfm.pdf
|
|
139
|
+
//
|
|
140
|
+
// Usage
|
|
141
|
+
//
|
|
142
|
+
// .URL "http://www.debian.org" "Debian" "."
|
|
143
|
+
//
|
|
144
|
+
// * First argument: the URL
|
|
145
|
+
// * Second argument: text to be hyperlinked
|
|
146
|
+
// * Third (optional) argument: text that needs to immediately trail the hyperlink without intervening whitespace
|
|
147
|
+
result.push(`.de URL
|
|
148
|
+
\\fI\\\\$2\\fP <\\\\$1>\\\\$3
|
|
149
|
+
..
|
|
150
|
+
.als MTO URL
|
|
151
|
+
.if \\n[.g] \\{\\
|
|
152
|
+
. mso www.tmac
|
|
153
|
+
. am URL
|
|
154
|
+
. ad l
|
|
155
|
+
. .
|
|
156
|
+
. am MTO
|
|
157
|
+
. ad l
|
|
158
|
+
. .`)
|
|
159
|
+
result.push(
|
|
160
|
+
`. LINKSTYLE ${node.getAttribute('man-linkstyle', 'blue R < >')}`
|
|
161
|
+
)
|
|
162
|
+
result.push('.\\}')
|
|
163
|
+
|
|
164
|
+
if (!node.isNoheader()) {
|
|
165
|
+
if (node.hasAttribute('manpurpose')) {
|
|
166
|
+
const mannames = node.getAttribute('mannames', [manname])
|
|
167
|
+
result.push(`.SH "${(node.getAttribute('manname-title', 'NAME')).toUpperCase()}"
|
|
168
|
+
${mannames.map((n) => this.manify(n).replace(/\\-/g, '-')).join(', ')} \\- ${this.manify(node.getAttribute('manpurpose'), { whitespace: 'normalize' })}`)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
result.push(await node.content())
|
|
173
|
+
|
|
174
|
+
// QUESTION should NOTES come after AUTHOR(S)?
|
|
175
|
+
this._appendFootnotes(result, node)
|
|
176
|
+
|
|
177
|
+
const authors = node.authors()
|
|
178
|
+
if (authors.length > 0) {
|
|
179
|
+
if (authors.length > 1) {
|
|
180
|
+
result.push('.SH "AUTHORS"')
|
|
181
|
+
for (const author of authors) {
|
|
182
|
+
result.push(`.sp\n${author.name}`)
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
result.push(`.SH "AUTHOR"\n.sp\n${authors[0].name}`)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return result.join(LF)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// NOTE embedded doesn't really make sense in the manpage backend
|
|
193
|
+
async convert_embedded(node) {
|
|
194
|
+
const result = [await node.content()]
|
|
195
|
+
this._appendFootnotes(result, node)
|
|
196
|
+
// QUESTION should we add an AUTHOR(S) section?
|
|
197
|
+
return result.join(LF)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async convert_section(node) {
|
|
201
|
+
let macro, stitle
|
|
202
|
+
if (node.level > 1) {
|
|
203
|
+
macro = 'SS'
|
|
204
|
+
// QUESTION why captioned title? why not when level == 1?
|
|
205
|
+
stitle = node.captionedTitle()
|
|
206
|
+
} else {
|
|
207
|
+
macro = 'SH'
|
|
208
|
+
stitle = this._uppercasePcdata(node.title)
|
|
209
|
+
}
|
|
210
|
+
return `.${macro} "${this.manify(stitle)}"\n${await node.content()}`
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async convert_admonition(node) {
|
|
214
|
+
const titleSuffix = node.hasTitle()
|
|
215
|
+
? `\\fP: ${this.manify(node.title)}`
|
|
216
|
+
: ''
|
|
217
|
+
return `.if n .sp
|
|
218
|
+
.RS 4
|
|
219
|
+
.it 1 an-trap
|
|
220
|
+
.nr an-no-space-flag 1
|
|
221
|
+
.nr an-break-flag 1
|
|
222
|
+
.br
|
|
223
|
+
.ps +1
|
|
224
|
+
.B ${node.getAttribute('textlabel')}${titleSuffix}
|
|
225
|
+
.ps -1
|
|
226
|
+
.br
|
|
227
|
+
${await this._encloseContent(node)}
|
|
228
|
+
.sp .5v
|
|
229
|
+
.RE`
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async convert_colist(node) {
|
|
233
|
+
const result = []
|
|
234
|
+
if (node.hasTitle()) {
|
|
235
|
+
result.push(`.sp\n.B ${this.manify(node.title)}\n.br`)
|
|
236
|
+
}
|
|
237
|
+
result.push('.TS\ntab(:);\nr lw(\\n(.lu*75u/100u).')
|
|
238
|
+
|
|
239
|
+
let num = 0
|
|
240
|
+
for (const item of node.getItems()) {
|
|
241
|
+
result.push(`\\fB(${++num})\\fP\\h'-2n':T{`)
|
|
242
|
+
result.push(this.manify(item.getText(), { whitespace: 'normalize' }))
|
|
243
|
+
if (item.hasBlocks()) result.push(await item.content())
|
|
244
|
+
result.push('T}')
|
|
245
|
+
}
|
|
246
|
+
result.push('.TE')
|
|
247
|
+
return result.join(LF)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// TODO implement horizontal (if it makes sense)
|
|
251
|
+
async convert_dlist(node) {
|
|
252
|
+
const result = []
|
|
253
|
+
if (node.hasTitle()) {
|
|
254
|
+
result.push(`.sp\n.B ${this.manify(node.title)}\n.br`)
|
|
255
|
+
}
|
|
256
|
+
let counter = 0
|
|
257
|
+
for (const [terms, dd] of node.getItems()) {
|
|
258
|
+
counter++
|
|
259
|
+
if (node.style === 'qanda') {
|
|
260
|
+
result.push(
|
|
261
|
+
`.sp\n${counter}. ${this.manify(terms.map((dt) => dt.getText()).join(' '))}\n.RS 4`
|
|
262
|
+
)
|
|
263
|
+
} else {
|
|
264
|
+
result.push(
|
|
265
|
+
`.sp\n${this.manify(terms.map((dt) => dt.getText()).join(', '), { whitespace: 'normalize' })}\n.RS 4`
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
if (dd) {
|
|
269
|
+
let hasText = false
|
|
270
|
+
if (dd.hasText()) {
|
|
271
|
+
result.push(this.manify(dd.getText(), { whitespace: 'normalize' }))
|
|
272
|
+
hasText = true
|
|
273
|
+
}
|
|
274
|
+
if (dd.hasBlocks()) {
|
|
275
|
+
let ddContent = await dd.content()
|
|
276
|
+
if (!hasText && ddContent.startsWith('.sp\n')) {
|
|
277
|
+
ddContent = ddContent.slice(4)
|
|
278
|
+
}
|
|
279
|
+
result.push(ddContent)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
result.push('.RE')
|
|
283
|
+
}
|
|
284
|
+
return result.join(LF)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async convert_example(node) {
|
|
288
|
+
const titleBlock = node.hasTitle()
|
|
289
|
+
? `.sp\n.B ${this.manify(node.captionedTitle())}\n.br`
|
|
290
|
+
: '.sp'
|
|
291
|
+
return `${titleBlock}\n.RS 4\n${await this._encloseContent(node)}\n.RE`
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async convert_floating_title(node) {
|
|
295
|
+
return `.SS "${this.manify(node.title)}"`
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async convert_image(node) {
|
|
299
|
+
const titleBlock = node.hasTitle()
|
|
300
|
+
? `.sp\n.B ${this.manify(node.captionedTitle())}\n.br`
|
|
301
|
+
: '.sp'
|
|
302
|
+
return `${titleBlock}\n[${this.manify(node.getAttribute('alt'))}]`
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async convert_listing(node) {
|
|
306
|
+
const result = []
|
|
307
|
+
if (node.hasTitle()) {
|
|
308
|
+
result.push(`.sp\n.B ${this.manify(node.captionedTitle())}\n.br`)
|
|
309
|
+
}
|
|
310
|
+
result.push(`.sp
|
|
311
|
+
.if n .RS 4
|
|
312
|
+
.nf
|
|
313
|
+
.fam C
|
|
314
|
+
${this.manify(await node.content(), { whitespace: 'preserve' })}
|
|
315
|
+
.fam
|
|
316
|
+
.fi
|
|
317
|
+
.if n .RE`)
|
|
318
|
+
return result.join(LF)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async convert_literal(node) {
|
|
322
|
+
const result = []
|
|
323
|
+
if (node.hasTitle()) {
|
|
324
|
+
result.push(`.sp\n.B ${this.manify(node.title)}\n.br`)
|
|
325
|
+
}
|
|
326
|
+
result.push(`.sp
|
|
327
|
+
.if n .RS 4
|
|
328
|
+
.nf
|
|
329
|
+
.fam C
|
|
330
|
+
${this.manify(await node.content(), { whitespace: 'preserve' })}
|
|
331
|
+
.fam
|
|
332
|
+
.fi
|
|
333
|
+
.if n .RE`)
|
|
334
|
+
return result.join(LF)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async convert_sidebar(node) {
|
|
338
|
+
const titleBlock = node.hasTitle()
|
|
339
|
+
? `.sp\n.B ${this.manify(node.title)}\n.br`
|
|
340
|
+
: '.sp'
|
|
341
|
+
return `${titleBlock}\n.RS 4\n${await this._encloseContent(node)}\n.RE`
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async convert_olist(node) {
|
|
345
|
+
const result = []
|
|
346
|
+
if (node.hasTitle()) {
|
|
347
|
+
result.push(`.sp\n.B ${this.manify(node.title)}\n.br`)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const start = parseInt(node.getAttribute('start', 1), 10)
|
|
351
|
+
let idx = 0
|
|
352
|
+
for (const item of node.getItems()) {
|
|
353
|
+
const numeral = idx + start
|
|
354
|
+
const listText = this.manify(item.getText(), { whitespace: 'normalize' })
|
|
355
|
+
result.push(`.sp
|
|
356
|
+
.RS 4
|
|
357
|
+
.ie n \\{\\
|
|
358
|
+
\\h'-04' ${numeral}.\\h'+01'\\c
|
|
359
|
+
.\\}
|
|
360
|
+
.el \\{\\
|
|
361
|
+
. sp -1
|
|
362
|
+
. IP " ${numeral}." 4.2
|
|
363
|
+
.\\}${listText === '' ? '' : LF + listText}`)
|
|
364
|
+
if (item.hasBlocks()) {
|
|
365
|
+
let itemContent = await item.content()
|
|
366
|
+
if (listText === '' && itemContent.startsWith('.sp\n')) {
|
|
367
|
+
itemContent = itemContent.slice(4)
|
|
368
|
+
}
|
|
369
|
+
result.push(itemContent)
|
|
370
|
+
}
|
|
371
|
+
result.push('.RE')
|
|
372
|
+
idx++
|
|
373
|
+
}
|
|
374
|
+
return result.join(LF)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async convert_open(node) {
|
|
378
|
+
if (node.style === 'abstract' || node.style === 'partintro') {
|
|
379
|
+
return this._encloseContent(node)
|
|
380
|
+
}
|
|
381
|
+
return await node.content()
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async convert_page_break(_node) {
|
|
385
|
+
return '.bp'
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async convert_paragraph(node) {
|
|
389
|
+
if (node.hasTitle()) {
|
|
390
|
+
return `.sp\n.B ${this.manify(node.title)}\n.br\n${this.manify(await node.content(), { whitespace: 'normalize' })}`
|
|
391
|
+
}
|
|
392
|
+
return `.sp\n${this.manify(await node.content(), { whitespace: 'normalize' })}`
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
async convert_pass(node) {
|
|
396
|
+
return this.contentOnly(node)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async convert_preamble(node) {
|
|
400
|
+
return this.contentOnly(node)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async convert_quote(node) {
|
|
404
|
+
const result = []
|
|
405
|
+
if (node.hasTitle()) {
|
|
406
|
+
result.push(`.sp\n.RS 3\n.B ${this.manify(node.title)}\n.br\n.RE`)
|
|
407
|
+
}
|
|
408
|
+
let attributionLine = node.hasAttribute('citetitle')
|
|
409
|
+
? `${node.getAttribute('citetitle')} `
|
|
410
|
+
: null
|
|
411
|
+
if (node.hasAttribute('attribution')) {
|
|
412
|
+
attributionLine = `${attributionLine ?? ''}\\(em ${node.getAttribute('attribution')}`
|
|
413
|
+
} else {
|
|
414
|
+
attributionLine = null
|
|
415
|
+
}
|
|
416
|
+
result.push(
|
|
417
|
+
`.RS 3\n.ll -.6i\n${await this._encloseContent(node)}\n.br\n.RE\n.ll`
|
|
418
|
+
)
|
|
419
|
+
if (attributionLine) {
|
|
420
|
+
result.push(`.RS 5\n.ll -.10i\n${attributionLine}\n.RE\n.ll`)
|
|
421
|
+
}
|
|
422
|
+
return result.join(LF)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async convert_stem(node) {
|
|
426
|
+
const result = []
|
|
427
|
+
result.push(
|
|
428
|
+
node.hasTitle() ? `.sp\n.B ${this.manify(node.title)}\n.br` : '.sp'
|
|
429
|
+
)
|
|
430
|
+
const style = node.style
|
|
431
|
+
const [open, close] = BLOCK_MATH_DELIMITERS[style] ?? ['', '']
|
|
432
|
+
let equation = await node.content()
|
|
433
|
+
if (equation.startsWith(open) && equation.endsWith(close)) {
|
|
434
|
+
equation = equation.slice(open.length, equation.length - close.length)
|
|
435
|
+
}
|
|
436
|
+
result.push(
|
|
437
|
+
`${this.manify(equation, { whitespace: 'preserve' })} (${style})`
|
|
438
|
+
)
|
|
439
|
+
return result.join(LF)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// NOTE This handler inserts empty cells to account for colspans and rowspans.
|
|
443
|
+
// In order to support colspans and rowspans properly, that information must
|
|
444
|
+
// be computed up front and consulted when rendering the cell as this information
|
|
445
|
+
// is not available on the cell itself.
|
|
446
|
+
async convert_table(node) {
|
|
447
|
+
const result = []
|
|
448
|
+
if (node.hasTitle()) {
|
|
449
|
+
result.push(`.sp
|
|
450
|
+
.it 1 an-trap
|
|
451
|
+
.nr an-no-space-flag 1
|
|
452
|
+
.nr an-break-flag 1
|
|
453
|
+
.br
|
|
454
|
+
.B ${this.manify(node.captionedTitle())}
|
|
455
|
+
`)
|
|
456
|
+
}
|
|
457
|
+
result.push(`.TS\nallbox tab(:);`)
|
|
458
|
+
|
|
459
|
+
const rowHeader = []
|
|
460
|
+
const rowText = []
|
|
461
|
+
let rowIndex = 0
|
|
462
|
+
|
|
463
|
+
for (const [tsec, rows] of node.rows.bySection()) {
|
|
464
|
+
if (rows.length === 0) continue
|
|
465
|
+
for (const row of rows) {
|
|
466
|
+
rowHeader[rowIndex] = rowHeader[rowIndex] ?? []
|
|
467
|
+
rowText[rowIndex] = rowText[rowIndex] ?? []
|
|
468
|
+
let remainingCells = row.length
|
|
469
|
+
let cellIndex = 0
|
|
470
|
+
for (const cell of row) {
|
|
471
|
+
remainingCells--
|
|
472
|
+
rowHeader[rowIndex][cellIndex] = rowHeader[rowIndex][cellIndex] ?? []
|
|
473
|
+
// add an empty cell as a placeholder if this is a rowspan cell
|
|
474
|
+
if (
|
|
475
|
+
JSON.stringify(rowHeader[rowIndex][cellIndex]) ===
|
|
476
|
+
JSON.stringify(['^t'])
|
|
477
|
+
) {
|
|
478
|
+
rowText[rowIndex].push(`T{${LF}T}:`)
|
|
479
|
+
}
|
|
480
|
+
rowText[rowIndex].push(`T{${LF}`)
|
|
481
|
+
const cellHalign = (cell.getAttribute('halign', 'left') ?? 'left')[0]
|
|
482
|
+
if (tsec === 'body') {
|
|
483
|
+
if (
|
|
484
|
+
rowHeader[rowIndex].length === 0 ||
|
|
485
|
+
rowHeader[rowIndex][cellIndex].length === 0
|
|
486
|
+
) {
|
|
487
|
+
rowHeader[rowIndex][cellIndex].push(`${cellHalign}t`)
|
|
488
|
+
} else {
|
|
489
|
+
rowHeader[rowIndex][cellIndex + 1] =
|
|
490
|
+
rowHeader[rowIndex][cellIndex + 1] ?? []
|
|
491
|
+
rowHeader[rowIndex][cellIndex + 1].push(`${cellHalign}t`)
|
|
492
|
+
}
|
|
493
|
+
let cellContent
|
|
494
|
+
if (cell.style === 'asciidoc') {
|
|
495
|
+
cellContent = await cell.content()
|
|
496
|
+
} else if (cell.style === 'literal') {
|
|
497
|
+
cellContent = `.nf${LF}${this.manify(cell.text, { whitespace: 'preserve' })}${LF}.fi`
|
|
498
|
+
} else {
|
|
499
|
+
cellContent = (await cell.content())
|
|
500
|
+
.map((p) => this.manify(p, { whitespace: 'normalize' }))
|
|
501
|
+
.join(`${LF}.sp${LF}`)
|
|
502
|
+
}
|
|
503
|
+
rowText[rowIndex].push(`${cellContent}${LF}`)
|
|
504
|
+
} else {
|
|
505
|
+
// tsec === 'head' || tsec === 'foot'
|
|
506
|
+
if (
|
|
507
|
+
rowHeader[rowIndex].length === 0 ||
|
|
508
|
+
rowHeader[rowIndex][cellIndex].length === 0
|
|
509
|
+
) {
|
|
510
|
+
rowHeader[rowIndex][cellIndex].push(`${cellHalign}tB`)
|
|
511
|
+
} else {
|
|
512
|
+
rowHeader[rowIndex][cellIndex + 1] =
|
|
513
|
+
rowHeader[rowIndex][cellIndex + 1] ?? []
|
|
514
|
+
rowHeader[rowIndex][cellIndex + 1].push(`${cellHalign}tB`)
|
|
515
|
+
}
|
|
516
|
+
rowText[rowIndex].push(
|
|
517
|
+
`${this.manify(cell.text, { whitespace: 'normalize' })}${LF}`
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
if (cell.colspan && cell.colspan > 1) {
|
|
521
|
+
for (let i = 0; i < cell.colspan - 1; i++) {
|
|
522
|
+
if (
|
|
523
|
+
rowHeader[rowIndex].length === 0 ||
|
|
524
|
+
rowHeader[rowIndex][cellIndex].length === 0
|
|
525
|
+
) {
|
|
526
|
+
rowHeader[rowIndex][cellIndex + i].push('st')
|
|
527
|
+
} else {
|
|
528
|
+
rowHeader[rowIndex][cellIndex + 1 + i] =
|
|
529
|
+
rowHeader[rowIndex][cellIndex + 1 + i] ?? []
|
|
530
|
+
rowHeader[rowIndex][cellIndex + 1 + i].push('st')
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (cell.rowspan && cell.rowspan > 1) {
|
|
535
|
+
for (let i = 0; i < cell.rowspan - 1; i++) {
|
|
536
|
+
rowHeader[rowIndex + 1 + i] = rowHeader[rowIndex + 1 + i] ?? []
|
|
537
|
+
if (
|
|
538
|
+
rowHeader[rowIndex + 1 + i].length === 0 ||
|
|
539
|
+
(rowHeader[rowIndex + 1 + i][cellIndex] ?? []).length === 0
|
|
540
|
+
) {
|
|
541
|
+
rowHeader[rowIndex + 1 + i][cellIndex] =
|
|
542
|
+
rowHeader[rowIndex + 1 + i][cellIndex] ?? []
|
|
543
|
+
rowHeader[rowIndex + 1 + i][cellIndex].push('^t')
|
|
544
|
+
} else {
|
|
545
|
+
rowHeader[rowIndex + 1 + i][cellIndex + 1] =
|
|
546
|
+
rowHeader[rowIndex + 1 + i][cellIndex + 1] ?? []
|
|
547
|
+
rowHeader[rowIndex + 1 + i][cellIndex + 1].push('^t')
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (remainingCells >= 1) {
|
|
552
|
+
rowText[rowIndex].push('T}:')
|
|
553
|
+
} else {
|
|
554
|
+
rowText[rowIndex].push(`T}${LF}`)
|
|
555
|
+
}
|
|
556
|
+
cellIndex++
|
|
557
|
+
}
|
|
558
|
+
rowIndex++
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
let rowTextSlice = rowText
|
|
563
|
+
if (node.hasHeaderOption && rowText[0]) {
|
|
564
|
+
result.push(`${LF}${rowHeader[0].join(' ')}.`)
|
|
565
|
+
result.push(`${LF}${rowText[0].join('')}`)
|
|
566
|
+
result.push('.T&')
|
|
567
|
+
rowTextSlice = rowText.slice(1)
|
|
568
|
+
}
|
|
569
|
+
result.push(`${LF}${rowHeader[0].map(() => 'lt').join(' ')}.${LF}`)
|
|
570
|
+
for (const row of rowTextSlice) result.push(row.join(''))
|
|
571
|
+
result.push(`.TE${LF}.sp`)
|
|
572
|
+
return result.join('')
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async convert_thematic_break(_node) {
|
|
576
|
+
return `.sp
|
|
577
|
+
.ce
|
|
578
|
+
\\l'\\n(.lu*25u/100u\\(ap'`
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
async convert_toc(_node) {
|
|
582
|
+
// skip
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async convert_ulist(node) {
|
|
586
|
+
const result = []
|
|
587
|
+
if (node.hasTitle()) {
|
|
588
|
+
result.push(`.sp\n.B ${this.manify(node.title)}\n.br`)
|
|
589
|
+
}
|
|
590
|
+
for (const item of node.getItems()) {
|
|
591
|
+
const listText = this.manify(item.getText(), { whitespace: 'normalize' })
|
|
592
|
+
result.push(`.sp
|
|
593
|
+
.RS 4
|
|
594
|
+
.ie n \\{\\
|
|
595
|
+
\\h'-04'\\(bu\\h'+03'\\c
|
|
596
|
+
.\\}
|
|
597
|
+
.el \\{\\
|
|
598
|
+
. sp -1
|
|
599
|
+
. IP \\(bu 2.3
|
|
600
|
+
.\\}${listText === '' ? '' : LF + listText}`)
|
|
601
|
+
if (item.hasBlocks()) {
|
|
602
|
+
let itemContent = await item.content()
|
|
603
|
+
if (listText === '' && itemContent.startsWith('.sp\n')) {
|
|
604
|
+
itemContent = itemContent.slice(4)
|
|
605
|
+
}
|
|
606
|
+
result.push(itemContent)
|
|
607
|
+
}
|
|
608
|
+
result.push('.RE')
|
|
609
|
+
}
|
|
610
|
+
return result.join(LF)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async convert_verse(node) {
|
|
614
|
+
const result = []
|
|
615
|
+
if (node.hasTitle()) {
|
|
616
|
+
result.push(`.sp\n.B ${this.manify(node.title)}\n.br`)
|
|
617
|
+
}
|
|
618
|
+
let attributionLine = node.hasAttribute('citetitle')
|
|
619
|
+
? `${node.getAttribute('citetitle')} `
|
|
620
|
+
: null
|
|
621
|
+
if (node.hasAttribute('attribution')) {
|
|
622
|
+
attributionLine = `${attributionLine ?? ''}\\(em ${node.getAttribute('attribution')}`
|
|
623
|
+
} else {
|
|
624
|
+
attributionLine = null
|
|
625
|
+
}
|
|
626
|
+
result.push(
|
|
627
|
+
`.sp\n.nf\n${this.manify(await node.content(), { whitespace: 'preserve' })}\n.fi\n.br`
|
|
628
|
+
)
|
|
629
|
+
if (attributionLine) {
|
|
630
|
+
result.push(`.in +.5i\n.ll -.5i\n${attributionLine}\n.in\n.ll`)
|
|
631
|
+
}
|
|
632
|
+
return result.join(LF)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async convert_video(node) {
|
|
636
|
+
const startParam = node.hasAttribute('start')
|
|
637
|
+
? `&start=${node.getAttribute('start')}`
|
|
638
|
+
: ''
|
|
639
|
+
const endParam = node.hasAttribute('end')
|
|
640
|
+
? `&end=${node.getAttribute('end')}`
|
|
641
|
+
: ''
|
|
642
|
+
const titleBlock = node.hasTitle()
|
|
643
|
+
? `.sp\n.B ${this.manify(node.title)}\n.br`
|
|
644
|
+
: '.sp'
|
|
645
|
+
return `${titleBlock}\n<${node.mediaUri(node.getAttribute('target'))}${startParam}${endParam}> (video)`
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
async convert_inline_anchor(node) {
|
|
649
|
+
const target = node.target
|
|
650
|
+
switch (node.type) {
|
|
651
|
+
case 'link': {
|
|
652
|
+
let macro
|
|
653
|
+
let resolvedTarget = target
|
|
654
|
+
if (target.startsWith('mailto:')) {
|
|
655
|
+
macro = 'MTO'
|
|
656
|
+
resolvedTarget = target.slice(7)
|
|
657
|
+
} else {
|
|
658
|
+
macro = 'URL'
|
|
659
|
+
}
|
|
660
|
+
let text = node.text
|
|
661
|
+
if (text === resolvedTarget) {
|
|
662
|
+
text = ''
|
|
663
|
+
} else {
|
|
664
|
+
text = text.replace(/"/g, `${ESC_BS}(dq`)
|
|
665
|
+
}
|
|
666
|
+
if (macro === 'MTO') {
|
|
667
|
+
resolvedTarget = resolvedTarget.replace('@', `${ESC_BS}(at`)
|
|
668
|
+
}
|
|
669
|
+
return `${ESC_BS}c${LF}${ESC_FS}${macro} "${resolvedTarget}" "${text}" `
|
|
670
|
+
}
|
|
671
|
+
case 'xref': {
|
|
672
|
+
let text = node.text
|
|
673
|
+
if (!text) {
|
|
674
|
+
const refs = (this._refs ??= node.document.catalog.refs)
|
|
675
|
+
const refid = node.attributes.refid
|
|
676
|
+
let top
|
|
677
|
+
const ref =
|
|
678
|
+
refs[refid] ?? (!refid ? (top = this._getRootDocument(node)) : null)
|
|
679
|
+
if (ref instanceof AbstractNode) {
|
|
680
|
+
const resolvingSet = (this._resolvingXrefs ??= new Set())
|
|
681
|
+
if (!resolvingSet.has(refid)) {
|
|
682
|
+
resolvingSet.add(refid)
|
|
683
|
+
const resolved = await ref.xreftext(
|
|
684
|
+
node.getAttribute('xrefstyle', null, true)
|
|
685
|
+
)
|
|
686
|
+
resolvingSet.delete(refid)
|
|
687
|
+
if (resolved) {
|
|
688
|
+
text = resolved
|
|
689
|
+
if (
|
|
690
|
+
ref.context === 'section' &&
|
|
691
|
+
ref.level < 2 &&
|
|
692
|
+
text === ref.title
|
|
693
|
+
) {
|
|
694
|
+
text = this._uppercasePcdata(text)
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
text = top ? '[^top]' : `[${refid}]`
|
|
698
|
+
}
|
|
699
|
+
} else {
|
|
700
|
+
text = top ? '[^top]' : `[${refid}]`
|
|
701
|
+
}
|
|
702
|
+
} else {
|
|
703
|
+
text = `[${refid}]`
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return text
|
|
707
|
+
}
|
|
708
|
+
case 'ref':
|
|
709
|
+
case 'bibref':
|
|
710
|
+
// These are anchor points, which shouldn't be visible
|
|
711
|
+
return ''
|
|
712
|
+
default:
|
|
713
|
+
this.logger.warn(`unknown anchor type: ${node.type}`)
|
|
714
|
+
return null
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async convert_inline_break(node) {
|
|
719
|
+
return `${node.text}${LF}${ESC_FS}br`
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async convert_inline_button(node) {
|
|
723
|
+
return `<${ESC_BS}fB>[${ESC_BS}0${node.text}${ESC_BS}0]</${ESC_BS}fP>`
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
async convert_inline_callout(node) {
|
|
727
|
+
return `<${ESC_BS}fB>(${node.text})<${ESC_BS}fP>`
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async convert_inline_footnote(node) {
|
|
731
|
+
const index = node.getAttribute('index')
|
|
732
|
+
if (index) return `[${index}]`
|
|
733
|
+
if (node.type === 'xref') return `[${node.text}]`
|
|
734
|
+
return null
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
async convert_inline_image(node) {
|
|
738
|
+
return node.hasAttribute('link')
|
|
739
|
+
? `[${node.getAttribute('alt')}] <${node.getAttribute('link')}>`
|
|
740
|
+
: `[${node.getAttribute('alt')}]`
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async convert_inline_indexterm(node) {
|
|
744
|
+
return node.type === 'visible' ? node.text : ''
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async convert_inline_kbd(node) {
|
|
748
|
+
const keys = node.getAttribute('keys')
|
|
749
|
+
return `<${ESC_BS}f(CR>${keys.length === 1 ? keys[0] : keys.join(`${ESC_BS}0+${ESC_BS}0`)}</${ESC_BS}fP>`
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
async convert_inline_menu(node) {
|
|
753
|
+
const caret = `${ESC_BS}0${ESC_BS}(fc${ESC_BS}0`
|
|
754
|
+
const menu = node.getAttribute('menu')
|
|
755
|
+
const submenus = node.getAttribute('submenus')
|
|
756
|
+
if (submenus && submenus.length > 0) {
|
|
757
|
+
const submenuPath = submenus
|
|
758
|
+
.map((item) => `<${ESC_BS}fI>${item}</${ESC_BS}fP>`)
|
|
759
|
+
.join(caret)
|
|
760
|
+
return `<${ESC_BS}fI>${menu}</${ESC_BS}fP>${caret}${submenuPath}${caret}<${ESC_BS}fI>${node.getAttribute('menuitem')}</${ESC_BS}fP>`
|
|
761
|
+
} else if (node.getAttribute('menuitem')) {
|
|
762
|
+
return `<${ESC_BS}fI>${menu}${caret}${node.getAttribute('menuitem')}</${ESC_BS}fP>`
|
|
763
|
+
} else {
|
|
764
|
+
return `<${ESC_BS}fI>${menu}</${ESC_BS}fP>`
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// NOTE use fake XML elements to prevent creating artificial word boundaries
|
|
769
|
+
async convert_inline_quoted(node) {
|
|
770
|
+
switch (node.type) {
|
|
771
|
+
case 'emphasis':
|
|
772
|
+
return `<${ESC_BS}fI>${node.text}</${ESC_BS}fP>`
|
|
773
|
+
case 'strong':
|
|
774
|
+
return `<${ESC_BS}fB>${node.text}</${ESC_BS}fP>`
|
|
775
|
+
case 'monospaced':
|
|
776
|
+
return `<${ESC_BS}f(CR>${node.text}</${ESC_BS}fP>`
|
|
777
|
+
case 'single':
|
|
778
|
+
return `<${ESC_BS}(oq>${node.text}</${ESC_BS}(cq>`
|
|
779
|
+
case 'double':
|
|
780
|
+
return `<${ESC_BS}(lq>${node.text}</${ESC_BS}(rq>`
|
|
781
|
+
default:
|
|
782
|
+
return node.text
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Class method: write stub man pages for alternate names
|
|
787
|
+
static async writeAlternatePages(mannames, manvolnum, target) {
|
|
788
|
+
if (!mannames || mannames.length <= 1) return
|
|
789
|
+
mannames = mannames.slice(1)
|
|
790
|
+
const manvolext = `.${manvolnum}`
|
|
791
|
+
const { dirname, basename, join } = await import('node:path')
|
|
792
|
+
const { writeFile } = await import('node:fs/promises')
|
|
793
|
+
const dir = dirname(target)
|
|
794
|
+
const base = basename(target)
|
|
795
|
+
for (const manname of mannames) {
|
|
796
|
+
await writeFile(join(dir, `${manname}${manvolext}`), `.so ${base}`)
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// ── Private helpers ───────────────────────────────────────────────────────────
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* @internal
|
|
804
|
+
* @private
|
|
805
|
+
*/
|
|
806
|
+
_appendFootnotes(result, node) {
|
|
807
|
+
if (!node.hasFootnotes() || node.hasAttribute('nofootnotes')) return
|
|
808
|
+
result.push('.SH "NOTES"')
|
|
809
|
+
for (const fn of node.footnotes) {
|
|
810
|
+
result.push(`.IP [${fn.index}]`)
|
|
811
|
+
// NOTE restore newline in escaped macro that gets removed by normalize_text in substitutor
|
|
812
|
+
let text = fn.text
|
|
813
|
+
if (text.includes(`${ESC}\\c ${ESC}.`)) {
|
|
814
|
+
text = this.manify(
|
|
815
|
+
`${text.replace(MalformedEscapedMacroRx, `$1${LF}$2`)} `,
|
|
816
|
+
{ whitespace: 'normalize' }
|
|
817
|
+
).replace(/ $/, '')
|
|
818
|
+
} else {
|
|
819
|
+
text = this.manify(text, { whitespace: 'normalize' })
|
|
820
|
+
}
|
|
821
|
+
result.push(text)
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Converts HTML entity references back to their original form, escapes
|
|
827
|
+
* special man characters and strips trailing whitespace.
|
|
828
|
+
*
|
|
829
|
+
* It's crucial that text only ever pass through manify once.
|
|
830
|
+
*
|
|
831
|
+
* @param {string} str - the string to convert
|
|
832
|
+
* @param {Object} [opts={}] - options to control processing
|
|
833
|
+
* @param {'preserve'|'normalize'|'collapse'} [opts.whitespace='collapse'] - how to handle whitespace:
|
|
834
|
+
* `'preserve'` preserves spaces (only expanding tabs);
|
|
835
|
+
* `'normalize'` removes spaces around newlines;
|
|
836
|
+
* `'collapse'` collapses adjacent whitespace to a single space
|
|
837
|
+
* @param {boolean} [opts.append_newline=false] - append a newline to the result
|
|
838
|
+
* @returns {string} the manified string
|
|
839
|
+
*/
|
|
840
|
+
manify(str, opts = {}) {
|
|
841
|
+
const whitespace = opts.whitespace ?? 'collapse'
|
|
842
|
+
if (whitespace === 'preserve') {
|
|
843
|
+
// expand tabs, then escape leading indentation (2+ spaces not at line start)
|
|
844
|
+
str = str
|
|
845
|
+
.replace(/\t/g, ET)
|
|
846
|
+
.replace(/ {2,}/g, (m, offset, str) =>
|
|
847
|
+
offset === 0 || str[offset - 1] === '\n' ? m : `${ESC_BS}&${m}`
|
|
848
|
+
)
|
|
849
|
+
} else if (whitespace === 'normalize') {
|
|
850
|
+
str = str.replace(WrappedIndentRx, LF)
|
|
851
|
+
} else {
|
|
852
|
+
// collapse: replace any run of whitespace chars with a single space
|
|
853
|
+
str = str.replace(/[\n\t ]+/g, ' ')
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
str = str
|
|
857
|
+
// literal backslash (not a troff escape sequence)
|
|
858
|
+
.replace(LiteralBackslashRx, (m, $1) => ($1 ? m : '\\(rs'))
|
|
859
|
+
// horizontal ellipsis (emulate appearance)
|
|
860
|
+
.replace(EllipsisCharRefRx, '.\\|.\\|.')
|
|
861
|
+
// leading . used in troff for macro call; replace with \&.
|
|
862
|
+
.replace(LeadingPeriodRx, '\\&.')
|
|
863
|
+
// drop orphaned \c escape lines, unescape troff macro, quote adjacent char, isolate macro line
|
|
864
|
+
.replace(EscapedMacroRx, (_m, $1, $2, $3) => {
|
|
865
|
+
const rest = $3.trimStart()
|
|
866
|
+
return rest === ''
|
|
867
|
+
? `.${$1}"${$2}"`
|
|
868
|
+
: `.${$1}"${$2.trimEnd()}"\n${rest}`
|
|
869
|
+
})
|
|
870
|
+
.replace(/-/g, '\\-')
|
|
871
|
+
.replace(/</g, '<')
|
|
872
|
+
.replace(/>/g, '>')
|
|
873
|
+
.replace(/+/g, '+') // plus sign
|
|
874
|
+
.replace(/ /g, '\\~') // non-breaking space
|
|
875
|
+
.replace(/©/g, '\\(co') // copyright sign
|
|
876
|
+
.replace(/®/g, '\\(rg') // registered sign
|
|
877
|
+
.replace(/™/g, '\\(tm') // trademark sign
|
|
878
|
+
.replace(/°/g, '\\(de') // degree sign
|
|
879
|
+
.replace(/ /g, ' ') // thin space
|
|
880
|
+
.replace(/–/g, '\\(en') // en dash
|
|
881
|
+
.replace(EmDashCharRefRx, '\\(em') // em dash
|
|
882
|
+
.replace(/‘/g, '\\(oq') // left single quotation mark
|
|
883
|
+
.replace(/’/g, '\\(cq') // right single quotation mark
|
|
884
|
+
.replace(/“/g, '\\(lq') // left double quotation mark
|
|
885
|
+
.replace(/”/g, '\\(rq') // right double quotation mark
|
|
886
|
+
.replace(/←/g, '\\(<-') // leftwards arrow
|
|
887
|
+
.replace(/→/g, '\\(->') // rightwards arrow
|
|
888
|
+
.replace(/⇐/g, '\\(lA') // leftwards double arrow
|
|
889
|
+
.replace(/⇒/g, '\\(rA') // rightwards double arrow
|
|
890
|
+
.replace(/​/g, '\\:') // zero width space
|
|
891
|
+
.replace(/&/g, '&') // literal ampersand (must come after other & replacements)
|
|
892
|
+
.replace(/'/g, '\\*(Aq') // apostrophe / neutral single quote
|
|
893
|
+
.replace(MockMacroRx, '$1') // remove mock boundary markers
|
|
894
|
+
.replace(/\u001b\\/g, '\\') // unescape troff backslash (ESC_BS → \)
|
|
895
|
+
.replace(/\u001b\./g, '.') // unescape full stop in troff commands (ESC_FS → .)
|
|
896
|
+
.trimEnd() // strip trailing space
|
|
897
|
+
|
|
898
|
+
return opts.append_newline ? `${str}${LF}` : str
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* @internal
|
|
903
|
+
* @private
|
|
904
|
+
*/
|
|
905
|
+
_uppercasePcdata(string) {
|
|
906
|
+
if (!XMLMarkupRx.test(string)) return string.toUpperCase()
|
|
907
|
+
// Reset lastIndex since XMLMarkupRx is stateless (no /g flag) but test() advances for sticky
|
|
908
|
+
return string.replace(PCDATAFilterRx, (_m, $1, $2) =>
|
|
909
|
+
$2 ? $2.toUpperCase() : $1
|
|
910
|
+
)
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* @internal
|
|
915
|
+
* @private
|
|
916
|
+
*/
|
|
917
|
+
async _encloseContent(node) {
|
|
918
|
+
return node.contentModel === 'compound'
|
|
919
|
+
? await node.content()
|
|
920
|
+
: `.sp\n${this.manify(await node.content(), { whitespace: 'normalize' })}`
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* @internal
|
|
925
|
+
* @private
|
|
926
|
+
*/
|
|
927
|
+
_getRootDocument(node) {
|
|
928
|
+
while ((node = node.document).isNested()) {
|
|
929
|
+
node = node.parentDocument
|
|
930
|
+
}
|
|
931
|
+
return node
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
ManPageConverter.registerFor('manpage')
|