-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpric
324 lines (283 loc) Β· 11.5 KB
/
pric
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
#!/usr/bin/env groovy
/* Copyright 2020 HernΓ‘n JosΓ© Cervera Manzanilla
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@Grab(group='org.asciidoctor', module='asciidoctorj', version='2.4.0')
@Grab(group='org.asciidoctor', module='asciidoctorj-pdf', version='1.5.3')
@Grab(group='info.picocli', module='picocli', version='4.4.0')
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import picocli.CommandLine.Parameters
import picocli.CommandLine.ArgGroup
import java.util.regex.Pattern
import org.asciidoctor.*
import org.asciidoctor.extension.*
import static org.asciidoctor.Asciidoctor.Factory.create
import static org.asciidoctor.AttributesBuilder.attributes
import static org.asciidoctor.OptionsBuilder.options
@Command(name = 'pric',
version = 'pric 1.1.1',
mixinStandardHelpOptions = true,
headerHeading = '%n',
header = 'Print multiple source code files to a single PDF file.',
synopsisHeading = '%n',
abbreviateSynopsis = true,
parameterListHeading = '%nParameters:%n',
optionListHeading = '%nOptions:%n',
exitCodeListHeading = '%nExit codes:%n',
exitCodeList = [
'0: Successful PDF generation.',
'1: No PDF was generated.'
],
// Pric example uses
footerHeading = '%nExamples:%n',
footer = """\
Notice that the default title of every document is the provided filename
(in the examples below, the title of most documents is 'out'). To set an
appropriate title different from the filename, use the option -t, as shown
in the example 3.
1. Include all Java and XML files on the current dir and its sub-dirs.
@|yellow pric out.pdf -r -e java,xml|@
2. Include all Markdown files and the LICENSE file found on the current directory.
@|yellow pric out.pdf -e md -f 'LICENSE'|@
Notice the single quotes around LICENSE. Always use single quotes around the
arg value of -f, to avoid the shell from evaluating the expression.
3. Include all SQL files in current dir and add cover page with title and author.
@|yellow pric out.pdf -e sql -a 'Hernan Cervera' -t 'Database design'|@
4. Include all SQL files in current dir and remove cover page from PDF.
@|yellow pric out.pdf -e sql -c|@
""")
class Pric implements Runnable {
@ArgGroup(exclusive = false, multiplicity = '1')
FilesFilter filesFilter
@Parameters(index = '0', paramLabel = 'file',
description = "Filename of the PDF, must end in '.pdf'.")
String outputFilename
@Option(names = ['-t', '--title'],
description = 'Title of the document. If omitted, filename is used.')
String title
@Option(names = ['-a', '--author'],
description = 'Author of the document.')
String author
@Option(names = ['-p', '--paths'], paramLabel = '<path>', split = '[?]', splitSynopsisLabel = '?',
description = 'Absolute or relative paths to directories to search for files (default is current dir)')
List<String> paths = ['.']
@Option(names = ['-r', '--recursive'],
description = 'Recurse sub-directories.')
boolean recursive
@Option(names = ['-k', '--keep-asciidoc'],
description = 'Preserve AsciiDoc file after PDF generation.')
boolean keepAsciidoc
@Option(names = ['-s', '--silent'],
description = 'Do not show log messages.')
boolean loggerSilent
@Option(names = ['-c', '--compact'], description = 'Produce a PDF with more code per page and no cover page.')
boolean compact
static class FilesFilter {
@Option(names = ['-e', '--extensions'], split = ',', paramLabel = '<ext>',
description = 'Extensions of the files to be printed.')
List<String> extensions
@Option(names = ['-f', '--files'], split = ':', paramLabel = '<regex>',
description = 'Match filenames by regex.')
List<Pattern> patterns
}
def ext2type = [
// Backend
java: 'java',
groovy: 'groovy',
php: 'php',
py: 'python',
rb: 'ruby',
clj: 'clojure',
go: 'go',
lua: 'lua',
// Frontend
js: 'javascript',
html: 'html',
css: 'css',
cljs: 'clojure',
// Markup
xml: 'xml',
json: 'json',
yml: 'yaml',
yaml: 'yaml',
// Query
sql: 'sql',
// Low level
c: 'c',
cpp: 'c++'
]
private List<Closure<Boolean>> matchStrategies() {
List<Closure<Boolean>> strategies = new ArrayList<>();
if (filesFilter.extensions) {
Closure<Boolean> strategy = { filename, fileExtension -> fileExtension in filesFilter.extensions }
strategies.add(strategy)
}
if (filesFilter.patterns) {
Closure<Boolean> strategy = {
filename, fileExtension -> {
boolean matches = false
for (def pattern : filesFilter.patterns) {
if ((filename =~ pattern).matches()) {
matches = true
break
}
}
return matches
}
}
strategies.add(strategy)
}
return strategies
}
@Override
void run() {
// Configure PDF printer
PricPDFPrinter printer = new PricPDFPrinter(
pric: this,
logger: new Logger(silent: loggerSilent),
matchStrategies: matchStrategies())
printer.print() // Generate files
}
static void main (String... args) {
new CommandLine(new Pric()).execute(args)
System.exit(0)
}
}
class PricPDFPrinter {
// Options and parameters bindings
Pric pric
// Logger
Logger logger
// Filename match strategies
List<Closure<Boolean>> matchStrategies
void print() {
// Check the file passed by -o has .pdf extension
if (!pric.outputFilename.endsWith('.pdf')) {
logger.error('File name supplied by -o is not a PDF file. ' +
'Verify the filename has extension \'.pdf\'.')
System.exit(1)
}
// If it has .pdf extension, generate the PDF file
File adocFile = buildAdocFile()
generatePDF(adocFile)
// Conditional cleanup
if (!pric.keepAsciidoc) {
adocFile.delete()
}
}
private void generatePDF(File adocFile) {
Asciidoctor asciidoctor = create()
Attributes attributes = attributes().sourceHighlighter('coderay').get()
Options options = options().inPlace(true).safe(SafeMode.UNSAFE).backend('pdf')
.attributes(attributes).get()
logger.info("Generating PDF: ${adocFile.absolutePath.replaceAll('[.]adoc', '.pdf')}")
asciidoctor.convertFile(adocFile, options)
logger.info('Successful PDF generation.')
}
private File buildAdocFile() {
// Filename without extension
String filePathNoExtension = pric.outputFilename.replaceAll(/[.]\w+/, '')
String basename = (filePathNoExtension =~ /[\w\d!,\s;()$%&#ΒΏ'{}\[\]~_`+-]+$/).findAll()[0]
// Create AsciiDoc file
File adocFile = new File("${filePathNoExtension}.adoc")
// Set file title, author and doctype
StringBuilder header = new StringBuilder("= ${pric.title ?: basename}\n")
if (pric.author) { header.append(":author: $pric.author").append('\n') }
header.append(':source-highlighter: coderay').append('\n')
if (!pric.compact) { header.append(':doctype: book').append('\n') }
else if (pric.author) { header.append('\n[.text-center]\n{author}\n') }
try {
adocFile.withWriter('utf-8') { writer ->
writer.write(header.toString())
}
} catch (FileNotFoundException e) {
logger.error("A directory in the supplied path $pric.outputFilename does not exist.")
System.exit(1)
}
// Add include directives
includeFilesInAdoc(pric.recursive, pric.paths, adocFile)
return adocFile
}
private void includeFilesInAdoc(boolean recursive, List<String> paths, File adocFile) {
if (includeFilesInAdocHelper(recursive, paths, adocFile, 0) == 0) {
logger.info('No files were found. Check the values of -e and -p (if supplied).')
adocFile.delete()
System.exit(1)
}
}
private int includeFilesInAdocHelper(boolean recursive, List<String> paths, File adocFile, int matched) {
for (String path : paths) {
File dir = new File(path)
// Skip non-existent path
if (!dir.exists()) {
logger.warn("Non existent path, skipping: $dir.absolutePath")
continue
}
// Skip if is not directory
if (!dir.isDirectory()) {
logger.warn("Path is not a directory, skipping: $dir.absolutePath")
continue
}
// Iterate files and build adoc file
dir.eachFile { file ->
if (file.isDirectory() && !file.name.startsWith('.') && recursive) {
matched = includeFilesInAdocHelper(recursive, [file.path], adocFile, matched)
}
if (!file.isDirectory()) {
def partedFilename = file.name.split('[.]')
String fileExtension = partedFilename.size() > 1 ? partedFilename[1] : ''
for (Closure<Boolean> strategy : matchStrategies) {
if (strategy.call(file.name, fileExtension)) {
logger.info("Adding to output: $file.absolutePath")
matched++
adocFile.withWriterAppend('utf-8') { writer ->
writer.writeLine("""
${ pric.compact ? ".$file.name" : "== [small]#$file.name#"}
[%autofit]
[source,${pric.ext2type[fileExtension] ?: 'text'}]
----
include::$file.absolutePath[]
----""".stripIndent())
}
break
}
}
}
}
}
return matched
}
}
// Helper logger class
class Logger {
boolean silent = false
final static String INFO = 'INFO'
final static String WARNING = 'WARNING'
final static String ERROR = 'ERROR'
private static String noticeTemplate(String notice) { "[$notice]" }
private void log(String notice, String msg, PrintStream stream) {
if (!silent) stream.println("${noticeTemplate(notice)} $msg")
}
void info(String msg) {
log(INFO, msg, System.out)
}
void warn(String msg) {
log(WARNING, msg, System.err)
}
void error(String msg) {
log(ERROR, msg, System.err)
}
}