View Javadoc
1   /**************************************************************************
2    *
3    * Copyright (c) 2019-2020 Yawg project contributors.
4    *
5    **************************************************************************/
6   
7   package com.varmateo.yawg.commonmark;
8   
9   import java.nio.file.Path;
10  import java.util.Optional;
11  import java.util.function.Function;
12  import java.util.regex.Pattern;
13  
14  import io.vavr.Lazy;
15  import io.vavr.collection.List;
16  import io.vavr.control.Try;
17  import org.commonmark.Extension;
18  import org.commonmark.ext.front.matter.YamlFrontMatterExtension;
19  import org.commonmark.node.Node;
20  import org.commonmark.parser.Parser;
21  import org.commonmark.renderer.html.HtmlRenderer;
22  
23  import com.varmateo.yawg.api.Result;
24  import com.varmateo.yawg.spi.PageBakeResult;
25  import com.varmateo.yawg.spi.PageBaker;
26  import com.varmateo.yawg.spi.PageContext;
27  import com.varmateo.yawg.spi.Template;
28  import com.varmateo.yawg.spi.TemplateContext;
29  import com.varmateo.yawg.util.FileUtils;
30  import com.varmateo.yawg.util.PageBakeResults;
31  import com.varmateo.yawg.util.Results;
32  
33  
34  /**
35   * A <code>Baker</code> that translates text files in Markdown format
36   * into HTML files.
37   */
38  public final class CommonMarkPageBaker
39          implements PageBaker {
40  
41  
42      private static final String NAME = "markdown";
43  
44      private static final Pattern RE_ADOC = Pattern.compile(".*\\.md$");
45  
46      private static final String TARGET_EXTENSION = ".html";
47  
48      private final Lazy<Parser> _markdownParser =
49              Lazy.of(this::createMarkdownParser);
50  
51      private final Lazy<HtmlRenderer> _htmlRenderer =
52              Lazy.of(this::createHtmlRenderer);
53  
54      private final Lazy<CommonMarkTemplateContextFactory> _templateContextFactory =
55              Lazy.of(this::createTemplateContextFactory);
56  
57  
58      private CommonMarkPageBaker() {
59          // Nothing to do.
60      }
61  
62  
63      /**
64       *
65       */
66      public static PageBaker create() {
67  
68          return new CommonMarkPageBaker();
69      }
70  
71  
72      /**
73       * {@inheritDoc}
74       */
75      @Override
76      public String shortName() {
77  
78          return NAME;
79      }
80  
81  
82      /**
83       * Checks if the given file name has one of the known extensions.
84       *
85       * <p>The following extensions will be allowed:</p>
86       *
87       * <ul>
88       *   <li>.md</li>
89       * </ul>
90       *
91       * @return True if the given file name has one of the allowed
92       * extensions.
93       */
94      @Override
95      public boolean isBakeable(final Path path) {
96  
97          return FileUtils.isNameMatch(path, RE_ADOC);
98      }
99  
100 
101     /**
102      * Converts the given text file in Asciidoc format into an HTML
103      * file.
104      *
105      * <p>The target directory must already exist. Otherwise an
106      * exception will be thrown.</p>
107      *
108      * @param sourcePath The file to be baked.
109      *
110      * @param context Provides the template for generating the target
111      * document. If no template is provided, then the default
112      * AsciidoctorJ document generator will be used.
113      *
114      * @param targetDir The directory where the source file will be
115      * copied to.
116      *
117      * @return A result signaling success of failure.
118      */
119     @Override
120     public PageBakeResult bake(
121             final Path sourcePath,
122             final PageContext context,
123             final Path targetDir) {
124 
125         final Try<Void> result = doBake(sourcePath, context, targetDir);
126 
127         return PageBakeResults.fromTry(result);
128     }
129 
130 
131     /**
132      *
133      */
134     private Try<Void> doBake(
135             final Path sourcePath,
136             final PageContext context,
137             final Path targetDir) {
138 
139         final Path targetPath = getTargetPath(sourcePath, targetDir);
140         final Optional<Template> template = context.templateFor(sourcePath);
141         final Try<Void> result;
142 
143         if ( template.isPresent() ) {
144             result = doBakeWithTemplate(
145                     sourcePath,
146                     context,
147                     targetDir,
148                     targetPath,
149                     template.get());
150         } else {
151             result = doBakeWithoutTemplate(
152                     sourcePath,
153                     targetPath);
154         }
155 
156         return result;
157     }
158 
159 
160     /**
161      *
162      */
163     private Path getTargetPath(
164             final Path sourcePath,
165             final Path targetDir) {
166 
167         final String sourceBasename = FileUtils.basename(sourcePath);
168         final String targetName = sourceBasename + TARGET_EXTENSION;
169 
170         return targetDir.resolve(targetName);
171     }
172 
173 
174     /**
175      *
176      */
177     private Try<Void> doBakeWithoutTemplate(
178             final Path sourcePath,
179             final Path targetPath) {
180 
181         return renderBody(sourcePath)
182                 .flatMap(body -> renderContentAndSave(sourcePath, targetPath, body));
183     }
184 
185 
186     private Try<String> renderBody(final Path sourcePath) {
187 
188         final Try<Node> document = FileUtils.safeReadFrom(
189                 sourcePath,
190                 reader -> _markdownParser.get().parseReader(reader));
191         final Try<String> body = document
192                 .map(doc -> _htmlRenderer.get().render(doc));
193 
194         return body
195                 .recoverWith(CommonMarkPageBakerException.commonMarkFailureTry(sourcePath));
196     }
197 
198 
199     private Try<Void> renderContentAndSave(
200             final Path sourcePath,
201             final Path targetPath,
202             final String body) {
203 
204         final String contentTemplate = ""
205                 + "<!DOCTYPE html>%n"
206                 + "<html><body>%s</body></html>";
207         final String content = String.format(contentTemplate, body);
208         final Try<Void> result = FileUtils.safeWriteTo(
209                 targetPath,
210                 writer -> writer.write(content));
211 
212         return result
213                 .recoverWith(CommonMarkPageBakerException.commonMarkFailureTry(sourcePath));
214     }
215 
216 
217     /**
218      *
219      */
220     private Try<Void> doBakeWithTemplate(
221             final Path sourcePath,
222             final PageContext context,
223             final Path targetDir,
224             final Path targetPath,
225             final Template template) {
226 
227         final Try<TemplateContext> templateContext = _templateContextFactory.get().build(
228                 sourcePath, targetDir, targetPath, context);
229 
230         return templateContext.flatMap(processTemplate(sourcePath, targetPath, template));
231     }
232 
233 
234     private Function<TemplateContext, Try<Void>> processTemplate(
235             final Path sourcePath,
236             final Path targetPath,
237             final Template template) {
238 
239         return (TemplateContext templateContext) -> {
240             final Try<Result<Void>> result = FileUtils.safeWriteWith(
241                     targetPath,
242                     writer -> template.process(templateContext, writer));
243 
244             return result
245                     .recoverWith(CommonMarkPageBakerException.templateFailureTry(sourcePath))
246                     .flatMap(Results::toTry);
247         };
248     }
249 
250 
251     private Parser createMarkdownParser() {
252 
253         final Extension frontMatterExtension = YamlFrontMatterExtension.create();
254         final List<Extension> extensions = List.of(frontMatterExtension);
255 
256         return Parser.builder()
257                 .extensions(extensions)
258                 .build();
259     }
260 
261 
262     private HtmlRenderer createHtmlRenderer() {
263 
264         return HtmlRenderer.builder().build();
265     }
266 
267 
268     private CommonMarkTemplateContextFactory createTemplateContextFactory() {
269 
270         return new CommonMarkTemplateContextFactory(_markdownParser.get(), _htmlRenderer.get());
271     }
272 
273 
274 }