001/** 002 * Copyright (C) 2006-2025 Talend Inc. - www.talend.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.talend.sdk.component.tools; 017 018import static java.util.Arrays.asList; 019import static java.util.Collections.emptyMap; 020import static java.util.Collections.list; 021import static java.util.Locale.ENGLISH; 022import static java.util.Optional.ofNullable; 023import static java.util.stream.Collectors.toList; 024import static org.apache.ziplock.JarLocation.jarLocation; 025 026import java.io.BufferedInputStream; 027import java.io.BufferedOutputStream; 028import java.io.File; 029import java.io.FileInputStream; 030import java.io.FileNotFoundException; 031import java.io.FileOutputStream; 032import java.io.IOException; 033import java.io.InputStream; 034import java.io.OutputStream; 035import java.nio.charset.StandardCharsets; 036import java.nio.file.Files; 037import java.nio.file.Path; 038import java.util.ArrayList; 039import java.util.Collection; 040import java.util.HashMap; 041import java.util.List; 042import java.util.Map; 043import java.util.jar.JarFile; 044import java.util.stream.Stream; 045 046import org.apache.ziplock.IO; 047import org.asciidoctor.Asciidoctor; 048import org.asciidoctor.AttributesBuilder; 049import org.asciidoctor.OptionsBuilder; 050import org.asciidoctor.SafeMode; 051import org.asciidoctor.jruby.internal.JRubyAsciidoctor; 052import org.talend.sdk.component.path.PathFactory; 053 054// indirection to not load asciidoctor if not in the classpath 055public class AsciidoctorExecutor implements AutoCloseable { 056 057 private Asciidoctor asciidoctor; 058 059 private File extractedResources; 060 061 private Runnable onClose = () -> { 062 }; 063 064 public static void main(final String[] args) throws IOException { 065 final Collection<String> params = new ArrayList<>(asList(args)); 066 try (final AsciidoctorExecutor executor = new AsciidoctorExecutor()) { 067 if (params.contains("--continue")) { 068 params.remove("--continue"); 069 final String[] newArgs = params.toArray(new String[0]); 070 do { 071 executor.doMain(newArgs); 072 073 final String line = System.console().readLine(); 074 if (line == null || "exit".equalsIgnoreCase(line.trim())) { 075 return; 076 } 077 executor.doMain(newArgs); 078 } while (true); 079 } else { 080 executor.doMain(args); 081 } 082 } 083 } 084 085 public void doMain(final String[] args) throws IOException { 086 final Path adoc = PathFactory.get(args[0]).toAbsolutePath(); 087 final File output = 088 adoc.getParent().resolve(args.length > 1 ? args[1] : args[0].replace(".adoc", ".pdf")).toFile(); 089 final List<String> lines = Files.lines(adoc).collect(toList()); 090 final String version = lines 091 .stream() 092 .filter(it -> it.startsWith("v")) 093 .findFirst() 094 .map(it -> it.substring(1)) 095 .orElse(args.length > 2 ? args[2] : ""); 096 render(args.length > 3 ? new File(args[3]) : new File("target/pdf"), version, new Log() { 097 098 @Override 099 public void debug(final String s) { 100 // no-op 101 } 102 103 @Override 104 public void error(final String s) { 105 System.err.println(s); 106 } 107 108 @Override 109 public void info(final String s) { 110 System.out.println(s); 111 } 112 }, "pdf", adoc.toFile(), output, 113 lines 114 .stream() 115 .filter(it -> it.startsWith("= ")) 116 .findFirst() 117 .orElseGet(() -> args.length > 4 ? args[4] : "Document"), 118 new HashMap<>(), args.length > 6 ? new File(args[6]) : null, args.length > 7 ? args[7] : null, 119 args.length > 5 ? args[5] : null); 120 } 121 122 public void render(final File workDir, final String version, final Log log, final String backend, final File source, 123 final File output, final String title, final Map<String, String> attributes, final File templateDir, 124 final String templateEngine) { 125 render(workDir, version, log, backend, source, output, title, attributes, templateDir, templateEngine, null); 126 } 127 128 // CHECKSTYLE:OFF 129 private void render(final File workDir, final String version, final Log log, final String backend, 130 final File source, final File output, final String title, final Map<String, String> attributes, 131 final File templateDir, final String templateEngine, final String libraries) { 132 // CHECKSTYLE:ON 133 log.info("Rendering '" + source.getName() + "' to '" + output + "'"); 134 if (asciidoctor == null) { 135 asciidoctor = getAsciidoctor(); 136 if (libraries != null) { 137 Stream 138 .of(libraries.split(",")) 139 .map(String::trim) 140 .filter(s -> !s.isEmpty()) 141 .forEach(asciidoctor::requireLibrary); 142 } 143 } 144 final OptionsBuilder optionsBuilder = OptionsBuilder 145 .options() 146 .baseDir(source.getParentFile()) 147 .toFile(output) 148 .backend(backend) 149 .safe(SafeMode.UNSAFE) 150 .docType("book") // todo: config 151 .mkDirs(true); 152 153 final AttributesBuilder attrs = 154 AttributesBuilder.attributes().attributeMissing("skip").attributeUndefined("drop"); 155 if (attributes == null) { 156 configureDefaultsAttributes(workDir, log, backend, attrs, emptyMap()); 157 } else { 158 attributes.forEach((k, v) -> { 159 if (v == null) { 160 attrs.attribute(k); 161 } else { 162 attrs.attribute(k, v); 163 } 164 }); 165 configureDefaultsAttributes(workDir, log, backend, attrs, attributes); 166 } 167 optionsBuilder.attributes(attrs); 168 169 ofNullable(templateDir).ifPresent(optionsBuilder::templateDir); 170 ofNullable(templateEngine).ifPresent(optionsBuilder::templateEngine); 171 172 log.debug("Options: " + optionsBuilder.asMap()); 173 asciidoctor.convert(wrap(title, version, source), optionsBuilder); 174 } 175 176 private void configureDefaultsAttributes(final File workDir, final Log log, final String backend, 177 final AttributesBuilder attrs, final Map<String, String> attributes) { 178 if (extractedResources == null) { 179 extractResources(workDir, log); 180 } 181 switch (backend.toLowerCase(ENGLISH)) { 182 case "html": 183 case "html5": 184 if (!attributes.containsKey("stylesheet")) { 185 attrs.attribute("stylesheet", "talend.css"); 186 } 187 if (!attributes.containsKey("stylesdir")) { 188 attrs.attribute("stylesdir", new File(extractedResources, "resources/html").getAbsolutePath()); 189 } 190 if (!attributes.containsKey("data-uri")) { 191 attrs.attribute("data-uri"); 192 } 193 break; 194 case "pdf": 195 if (!attributes.containsKey("pdf-style")) { 196 attrs.attribute("pdf-style", "talend.yml"); 197 } 198 if (!attributes.containsKey("pdf-stylesdir")) { 199 attrs.attribute("pdf-stylesdir", new File(extractedResources, "resources/pdf").getAbsolutePath()); 200 } 201 break; 202 default: 203 } 204 205 if (!attributes.containsKey("icons")) { 206 attrs.attribute("icons", "font"); 207 } 208 if (!attributes.containsKey("source-highlighter")) { 209 attrs.attribute("source-highlighter", "coderay"); 210 } 211 if (!attributes.containsKey("toc")) { 212 attrs.attribute("toc", "left"); 213 } 214 } 215 216 private void extractResources(final File workDir, final Log log) { 217 final boolean workdirCreated = !workDir.exists(); 218 workDir.mkdirs(); 219 extractedResources = new File(workDir, getClass().getSimpleName() + "_resources"); 220 onClose = () -> { 221 try { 222 org.apache.ziplock.Files.remove(workdirCreated ? workDir : extractedResources); 223 } catch (final IllegalStateException e) { 224 log.error(e.getMessage()); 225 } 226 }; 227 final File file = jarLocation(AsciidoctorExecutor.class); 228 if (file.isDirectory()) { 229 Stream.of("html", "pdf").forEach(backend -> { 230 final File[] children = new File(file, "resources/" + backend).listFiles(); 231 if (children == null) { 232 throw new IllegalStateException("No resources folder for: " + backend); 233 } 234 Stream.of(children).forEach(child -> { 235 try { 236 copyResource(new File(extractedResources, "resources/" + backend + '/' + child.getName()), 237 new FileInputStream(child)); 238 } catch (final FileNotFoundException e) { 239 throw new IllegalStateException(e); 240 } 241 }); 242 }); 243 } else { 244 try (final JarFile jar = new JarFile(file)) { 245 list(jar.entries()) 246 .stream() 247 .filter(e -> e.getName().startsWith("resources/") && !e.isDirectory()) 248 .forEach(e -> { 249 try { 250 copyResource(new File(extractedResources, e.getName()), jar.getInputStream(e)); 251 } catch (final IOException e1) { 252 throw new IllegalStateException(e1); 253 } 254 }); 255 } catch (final IOException e) { 256 throw new IllegalStateException(e); 257 } 258 } 259 } 260 261 private void copyResource(final File out, final InputStream inputStream) { 262 out.getParentFile().mkdirs(); 263 try (final InputStream is = new BufferedInputStream(inputStream); 264 final OutputStream os = new BufferedOutputStream(new FileOutputStream(out))) { 265 IO.copy(is, os); 266 } catch (final IOException e1) { 267 throw new IllegalStateException(e1); 268 } 269 } 270 271 private String wrap(final String title, final String version, final File source) { 272 try { 273 final String content = String.join("\n", Files.readAllLines(source.toPath(), StandardCharsets.UTF_8)); 274 if (content.startsWith("= ")) { 275 return content; 276 } 277 return "= " + title + "\n:revnumber: " + version + "\n\n" + content; 278 } catch (final IOException e) { 279 throw new IllegalArgumentException(e); 280 } 281 } 282 283 protected Asciidoctor getAsciidoctor() { 284 return asciidoctor == null ? asciidoctor = Asciidoctor.Factory.create() : asciidoctor; 285 } 286 287 @Override 288 public void close() { 289 onClose.run(); 290 if (asciidoctor != null && !Boolean.getBoolean("talend.component.tools.jruby.teardown.skip")) { 291 if (AutoCloseable.class.isInstance(asciidoctor)) { 292 try { 293 AutoCloseable.class.cast(asciidoctor).close(); 294 } catch (final Exception e) { 295 throw new IllegalStateException(e); 296 } 297 } else if (org.asciidoctor.jruby.internal.JRubyAsciidoctor.class.isInstance(asciidoctor)) { 298 JRubyAsciidoctor.class.cast(asciidoctor).getRubyRuntime().tearDown(); 299 } 300 } 301 } 302}