implemented flexmark & commonmark engines

This commit is contained in:
brian 2021-01-18 15:48:46 -05:00
parent 0d322c7ec9
commit b448025ee2
10 changed files with 478 additions and 53 deletions

View File

@ -17,6 +17,8 @@
<ats.version>2.3.6</ats.version> <ats.version>2.3.6</ats.version>
<spring-boot.version>2.3.5.RELEASE</spring-boot.version> <spring-boot.version>2.3.5.RELEASE</spring-boot.version>
<activemq.version>5.15.8</activemq.version> <activemq.version>5.15.8</activemq.version>
<commonmark.version>0.17.0</commonmark.version>
<flexmark.version>0.62.2</flexmark.version>
<image.registry>docker.yateslong.us</image.registry> <image.registry>docker.yateslong.us</image.registry>
<image.name>inteligr8/${project.artifactId}</image.name> <image.name>inteligr8/${project.artifactId}</image.name>
@ -24,6 +26,36 @@
</properties> </properties>
<dependencies> <dependencies>
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark</artifactId>
<version>${commonmark.version}</version>
</dependency>
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark-ext-gfm-tables</artifactId>
<version>${commonmark.version}</version>
</dependency>
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark-ext-task-list-items</artifactId>
<version>${commonmark.version}</version>
</dependency>
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark-ext-image-attributes</artifactId>
<version>${commonmark.version}</version>
</dependency>
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark-ext-ins</artifactId>
<version>${commonmark.version}</version>
</dependency>
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark-all</artifactId>
<version>${flexmark.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.alfresco</groupId> <groupId>org.alfresco</groupId>
<artifactId>alfresco-transformer-base</artifactId> <artifactId>alfresco-transformer-base</artifactId>

View File

@ -0,0 +1,184 @@
package com.inteligr8.alfresco.module.alfmarkdown;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.PostConstruct;
import org.alfresco.transform.exceptions.TransformException;
import org.alfresco.transformer.executors.Transformer;
import org.commonmark.Extension;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.Renderer;
import org.commonmark.renderer.html.HtmlRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
@Component
public class CommonmarkTransformer implements Transformer {
private final Logger logger = LoggerFactory.getLogger(CommonmarkTransformer.class);
private final String id = "commonmark";
private final List<String> classSearchPrefixes = Arrays.asList("", "org.commonmark.ext.{name}.", "org.commonmark.ext.gfm.{name}.");
private final Pattern extClassNamePattern = Pattern.compile("[\\.]?(([A-Za-z0-9]+)Extension|[A-Za-z0-9]+)$");
@Value("${transform.alfmarkdown.commonmark.defaultProfile}")
private String profile;
@Value("${transform.alfmarkdown.commonmark.defaultExtensions}")
private List<String> extensionClassNames;
@PostConstruct
public void init() throws Exception {
if (this.logger.isDebugEnabled())
this.logger.debug("init()");
}
@Override
public String getTransformerId() {
return this.id;
}
@Override
public void extractMetadata(String transformName, String sourceMimetype, String targetMimetype, Map<String, String> transformOptions, File sourceFile, File targetFile)
throws IOException {
if (this.logger.isTraceEnabled())
this.logger.trace("extractMetadata('" + transformName + "', '" + sourceMimetype + "', '" + targetMimetype + "', " + transformOptions.keySet() + ", '" + sourceFile + "', '" + targetFile + "')");
throw new TransformException(HttpStatus.NOT_IMPLEMENTED.value(), "This transformer does not support meta-data extraction");
}
@Override
public void transform(String transformName, String sourceMimetype, String targetMimetype, Map<String, String> transformOptions, File sourceFile, File targetFile)
throws IOException {
if (this.logger.isTraceEnabled())
this.logger.trace("transform('" + transformName + "', '" + sourceMimetype + "', '" + targetMimetype + "', " + transformOptions.keySet() + ", '" + sourceFile + "', '" + targetFile + "')");
List<Extension> extensions = this.gatherExtensions(transformOptions);
Parser parser = Parser.builder().extensions(extensions).build();
Renderer renderer;
switch (targetMimetype) {
case MediaType.TEXT_HTML_VALUE:
renderer = HtmlRenderer.builder().extensions(extensions).build();
break;
default:
throw new TransformException(HttpStatus.BAD_REQUEST.value(), "This transformer does not support target data of the '" + targetMimetype + "' type");
}
FileWriter fwriter = new FileWriter(targetFile, false);
try {
FileReader freader = new FileReader(sourceFile);
try {
Node mddoc = parser.parseReader(freader);
renderer.render(mddoc, fwriter);
} finally {
freader.close();
}
} finally {
fwriter.close();
}
}
private List<Extension> gatherExtensions(Map<String, String> transformOptions) {
// include default extensions
Set<String> extClassNames = new HashSet<>();
if (this.extensionClassNames != null)
extClassNames.addAll(this.extensionClassNames);
// include/exclude based on passed in extensions
String extClassNamesRaw = transformOptions.get(RequestParamConstants.EXT_CLASS_NAMES);
if (extClassNamesRaw != null) {
for (String extClassName : extClassNamesRaw.split(",")) {
extClassName = extClassName.trim();
if (extClassName.length() > 0) {
if (extClassName.startsWith("!")) {
// exclude those that start with !
extClassNames.remove(extClassName.substring(1));
} else {
// include the rest
extClassNames.add(extClassName);
}
}
}
}
List<Extension> exts = new ArrayList<>(extClassNames.size());
// create the extension classes using reflection
for (String extClassName : extClassNames) {
Class<?> extClass = this.findClass(extClassName);
if (extClass != null) {
Extension ext = this.createExtension(extClass);
if (ext != null)
exts.add(ext);
}
}
return exts;
}
private Class<?> findClass(String className) {
Matcher matcher = this.extClassNamePattern.matcher(className);
String extName = matcher.find() ? matcher.group(2) : null;
String extPackageName = this.camel2package(extName);
for (String prefix : this.classSearchPrefixes) {
String classPrefix = prefix.replaceAll("\\{name\\}", extPackageName);
try {
Class<?> extClass = Class.forName(classPrefix + className);
return extClass;
} catch (ClassNotFoundException cnfe) {
// suppress
}
}
return null;
}
private Extension createExtension(Class<?> extClass) {
try {
// finding public static method, so using "DeclaredMethod" instead of "Method"
Method createMethod = extClass.getDeclaredMethod("create");
// calling public static method, so no instance (null)
return (Extension)createMethod.invoke(null);
} catch (NoSuchMethodException nsme) {
this.logger.warn("The '" + extClass.getName() + "' extension class must have a public static 'create' method");
} catch (InvocationTargetException ite) {
this.logger.warn("The '" + extClass.getName() + "' extension class 'create' method encountered an unexpected exception: " + ite.getMessage());
} catch (IllegalAccessException iae) {
this.logger.warn("The '" + extClass.getName() + "' extension class 'create' method is not public: " + iae.getMessage());
}
return null;
}
private String camel2package(String str) {
StringJoiner joiner = new StringJoiner(".");
Pattern pattern = Pattern.compile("[A-Z][^A-Z]+");
Matcher matcher = pattern.matcher(str);
while (matcher.find())
joiner.add(matcher.group().toLowerCase());
return joiner.toString();
}
}

View File

@ -0,0 +1,180 @@
package com.inteligr8.alfresco.module.alfmarkdown;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.PostConstruct;
import org.alfresco.transform.exceptions.TransformException;
import org.alfresco.transformer.executors.Transformer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.parser.ParserEmulationProfile;
import com.vladsch.flexmark.util.ast.Document;
import com.vladsch.flexmark.util.ast.IRender;
import com.vladsch.flexmark.util.data.MutableDataSet;
import com.vladsch.flexmark.util.misc.Extension;
@Component
public class FlexmarkTransformer implements Transformer {
private final Logger logger = LoggerFactory.getLogger(FlexmarkTransformer.class);
private final String id = "flexmark";
private final List<String> classSearchPrefixes = Arrays.asList("", "com.vladsch.flexmark.ext.{name}.", "com.vladsch.flexmark.ext.");
private final Pattern extClassNamePattern = Pattern.compile("[\\.]?(([A-Za-z0-9]+)Extension|[A-Za-z0-9]+)$");
@Value("${transform.alfmarkdown.flexmark.defaultProfile}")
private String profile;
@Value("${transform.alfmarkdown.flexmark.defaultExtensions}")
private List<String> extensionClassNames;
@PostConstruct
public void init() throws Exception {
if (this.logger.isDebugEnabled())
this.logger.debug("init()");
}
@Override
public String getTransformerId() {
return this.id;
}
@Override
public void extractMetadata(String transformName, String sourceMimetype, String targetMimetype, Map<String, String> transformOptions, File sourceFile, File targetFile)
throws IOException {
if (this.logger.isTraceEnabled())
this.logger.trace("extractMetadata('" + transformName + "', '" + sourceMimetype + "', '" + targetMimetype + "', " + transformOptions.keySet() + ", '" + sourceFile + "', '" + targetFile + "')");
throw new TransformException(HttpStatus.NOT_IMPLEMENTED.value(), "This transformer does not support meta-data extraction");
}
@Override
public void transform(String transformName, String sourceMimetype, String targetMimetype, Map<String, String> transformOptions, File sourceFile, File targetFile)
throws IOException {
if (this.logger.isTraceEnabled())
this.logger.trace("transform('" + transformName + "', '" + sourceMimetype + "', '" + targetMimetype + "', " + transformOptions.keySet() + ", '" + sourceFile + "', '" + targetFile + "')");
String profile = transformOptions.getOrDefault(RequestParamConstants.PROFILE, this.profile);
MutableDataSet options = new MutableDataSet();
options.setFrom(ParserEmulationProfile.valueOf(profile.toUpperCase()));
options.set(Parser.EXTENSIONS, this.gatherExtensions(transformOptions));
Parser parser = Parser.builder(options).build();
IRender renderer;
switch (targetMimetype) {
case MediaType.TEXT_HTML_VALUE:
renderer = HtmlRenderer.builder(options).build();
break;
default:
throw new TransformException(HttpStatus.BAD_REQUEST.value(), "This transformer does not support target data of the '" + targetMimetype + "' type");
}
FileWriter fwriter = new FileWriter(targetFile, false);
try {
FileReader freader = new FileReader(sourceFile);
try {
Document mddoc = parser.parseReader(freader);
renderer.render(mddoc, fwriter);
} finally {
freader.close();
}
} finally {
fwriter.close();
}
}
private List<Extension> gatherExtensions(Map<String, String> transformOptions) {
// include default extensions
Set<String> extClassNames = new HashSet<>();
if (this.extensionClassNames != null)
extClassNames.addAll(this.extensionClassNames);
// include/exclude based on passed in extensions
String extClassNamesRaw = transformOptions.get(RequestParamConstants.EXT_CLASS_NAMES);
if (extClassNamesRaw != null) {
for (String extClassName : extClassNamesRaw.split("[,\\n]")) {
extClassName = extClassName.trim();
if (extClassName.length() > 0) {
if (extClassName.startsWith("!")) {
// exclude those that start with !
extClassNames.remove(extClassName.substring(1));
} else {
// include the rest
extClassNames.add(extClassName);
}
}
}
}
List<Extension> exts = new ArrayList<>(extClassNames.size());
// create the extension classes using reflection
for (String extClassName : extClassNames) {
Class<?> extClass = this.findClass(extClassName);
if (extClass != null) {
Extension ext = this.createExtension(extClass);
if (ext != null)
exts.add(ext);
}
}
return exts;
}
private Class<?> findClass(String className) {
Matcher matcher = this.extClassNamePattern.matcher(className);
String extName = matcher.find() ? matcher.group(2).toLowerCase() : null;
for (String prefix : this.classSearchPrefixes) {
String classPrefix = prefix.replaceAll("\\{name\\}", extName);
try {
Class<?> extClass = Class.forName(classPrefix + className);
return extClass;
} catch (ClassNotFoundException cnfe) {
// suppress
}
}
return null;
}
private Extension createExtension(Class<?> extClass) {
try {
// finding public static method, so using "DeclaredMethod" instead of "Method"
Method createMethod = extClass.getDeclaredMethod("create");
// calling public static method, so no instance (null)
return (Extension)createMethod.invoke(null);
} catch (NoSuchMethodException nsme) {
this.logger.warn("The '" + extClass.getName() + "' extension class must have a public static 'create' method");
} catch (InvocationTargetException ite) {
this.logger.warn("The '" + extClass.getName() + "' extension class 'create' method encountered an unexpected exception: " + ite.getMessage());
} catch (IllegalAccessException iae) {
this.logger.warn("The '" + extClass.getName() + "' extension class 'create' method is not public: " + iae.getMessage());
}
return null;
}
}

View File

@ -0,0 +1,9 @@
package com.inteligr8.alfresco.module.alfmarkdown;
public interface RequestParamConstants {
public static final String ENGINE = "engine";
public static final String PROFILE = "profile";
public static final String EXT_CLASS_NAMES = "extensions";
}

View File

@ -35,12 +35,15 @@ import java.util.regex.Pattern;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import org.alfresco.transform.exceptions.TransformException;
import org.alfresco.transformer.AbstractTransformerController; import org.alfresco.transformer.AbstractTransformerController;
import org.alfresco.transformer.executors.Transformer;
import org.alfresco.transformer.probes.ProbeTestTransform; import org.alfresco.transformer.probes.ProbeTestTransform;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
@ -69,9 +72,10 @@ public class TransformerController extends AbstractTransformerController {
private final Logger logger = LoggerFactory.getLogger(TransformerController.class); private final Logger logger = LoggerFactory.getLogger(TransformerController.class);
private final Pattern fileext = Pattern.compile("\\.([^\\.]+)$"); private final Pattern fileext = Pattern.compile("\\.([^\\.]+)$");
private final MediaType defaultTarget = MediaType.TEXT_HTML;
@Autowired @Autowired
private TransformerImpl transformer; private Transformer transformer;
@Value("${transform.alfmarkdown.version}") @Value("${transform.alfmarkdown.version}")
private String version; private String version;
@ -127,17 +131,30 @@ public class TransformerController extends AbstractTransformerController {
} }
if (targetMimetype == null) { if (targetMimetype == null) {
Matcher matcher = this.fileext.matcher(targetFile.getAbsolutePath()); Matcher matcher = this.fileext.matcher(targetFile.getAbsolutePath());
targetMimetype = matcher.find() ? this.ext2mime(matcher.group(1)) : MediaType.TEXT_PLAIN_VALUE; targetMimetype = matcher.find() ? this.ext2mime(matcher.group(1)) : this.defaultTarget.toString();
} }
this.transformer.transform(transformName, sourceMimetype, targetMimetype, transformOptions, sourceFile, targetFile); if (!MediaType.TEXT_MARKDOWN_VALUE.equals(sourceMimetype))
throw new TransformException(HttpStatus.BAD_REQUEST.value(), "This transformer does not support source data of the '" + sourceMimetype + "' type");
try {
this.transformer.transform(transformName, sourceMimetype, targetMimetype, transformOptions, sourceFile, targetFile);
} catch (Exception e) {
this.logger.error("The transformation encountered an unexpected issue", e);
throw new TransformException(HttpStatus.INTERNAL_SERVER_ERROR.value(), "The transformer encountered an unexpected");
}
} }
private String ext2mime(String ext) { private String ext2mime(String ext) {
switch (ext.toLowerCase()) { switch (ext.toLowerCase()) {
// add applicable extensions here // add applicable extensions here
case "md":
case "markdown": return MediaType.TEXT_MARKDOWN_VALUE;
case "html":
case "htm": return MediaType.TEXT_HTML_VALUE;
case "text": case "text":
case "txt": return MediaType.TEXT_PLAIN_VALUE; case "txt": return MediaType.TEXT_PLAIN_VALUE;
case "pdf": return MediaType.APPLICATION_PDF_VALUE;
default: return null; default: return null;
} }
} }

View File

@ -1,45 +0,0 @@
package com.inteligr8.alfresco.module.alfmarkdown;
import java.io.File;
import java.util.Map;
import javax.annotation.PostConstruct;
import org.alfresco.transformer.executors.Transformer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class TransformerImpl implements Transformer {
private final Logger logger = LoggerFactory.getLogger(TransformerImpl.class);
private final String id = "alfmarkdown";
@PostConstruct
public void init() throws Exception {
if (this.logger.isDebugEnabled())
this.logger.debug("init()");
}
@Override
public String getTransformerId() {
return this.id;
}
@Override
public void extractMetadata(String transformName, String sourceMimetype, String targetMimetype, Map<String, String> transformOptions, File sourceFile, File targetFile) {
if (this.logger.isTraceEnabled())
this.logger.trace("extractMetadata('" + transformName + "', '" + sourceMimetype + "', '" + targetMimetype + "', " + transformOptions.keySet() + ", '" + sourceFile + "', '" + targetFile + "')");
this.transform(transformName, sourceMimetype, targetMimetype, transformOptions, sourceFile, targetFile);
}
@Override
public void transform(String transformName, String sourceMimetype, String targetMimetype, Map<String, String> transformOptions, File sourceFile, File targetFile) {
if (this.logger.isTraceEnabled())
this.logger.trace("transform('" + transformName + "', '" + sourceMimetype + "', '" + targetMimetype + "', " + transformOptions.keySet() + ", '" + sourceFile + "', '" + targetFile + "')");
// TODO implement your transformation logic here
}
}

View File

@ -7,7 +7,29 @@ transform:
location: classpath:this_engine_config.json location: classpath:this_engine_config.json
alfmarkdown: alfmarkdown:
version: ${project.version} version: ${project.version}
commonmark:
defaultExtensions:
- ImageAttributesExtension
- TaskListItemsExtension
- TablesExtension
- InsExtension
flexmark:
defaultProfile: github
defaultExtensions:
- AdmonitionExtension
- DefinitionExtension
- EmojiExtension
- EnumeratedReferenceExtension
- FootnoteExtension
- InsExtension
- MediaTagsExtension
- SubscriptExtension
- SuperscriptExtension
- TablesExtension
- TaskListExtension
- TypographicExtension
- YouTubeLinkExtension
logging: logging:
level: level:
com.inteligr8.alfresco.module: ${LOG_LEVEL:info} com.inteligr8.alfresco.module.alfmarkdown: ${LOG_LEVEL:info}

View File

@ -7,7 +7,9 @@
<table> <table>
<tr><td><div style="text-align:right">file *</div></td><td><input type="file" name="file" /></td></tr> <tr><td><div style="text-align:right">file *</div></td><td><input type="file" name="file" /></td></tr>
<tr><td><div style="text-align:right">targetExtension *</div></td><td><input type="text" name="targetExtension" value="" /></td></tr> <tr><td><div style="text-align:right">targetExtension *</div></td><td><input type="text" name="targetExtension" value="" /></td></tr>
<!-- Add a row for each of your transform options --> <tr><td><div style="text-align:right">engine</div></td><td><input type="text" name="engine" value="" /></td></tr>
<tr><td><div style="text-align:right">profile</div></td><td><input type="text" name="profile" value="" /></td></tr>
<tr><td><div style="text-align:right">extensions</div></td><td><textarea name="extensions" rows="5"></textarea></tr>
<tr><td></td><td><input type="submit" value="Transform" /></td></tr> <tr><td></td><td><input type="submit" value="Transform" /></td></tr>
</table> </table>
</form> </form>

View File

@ -1,14 +1,16 @@
{ {
"transformOptions": { "transformOptions": {
"alfmarkdownOptions": [ "alfmarkdownOptions": [
// {"value": {"name": "profile"}}, {"value": {"name": "engine"}},
{"value": {"name": "profile"}},
{"value": {"name": "extensions"}}
] ]
}, },
"transformers": [ "transformers": [
{ {
"transformerName": "alfmarkdown", "transformerName": "alfmarkdown",
"supportedSourceAndTargetList": [ "supportedSourceAndTargetList": [
// {"sourceMediaType": "text/plain", "priority": 10, "targetMediaType": "text/plain" } {"sourceMediaType": "text/markdown", "priority": 10, "targetMediaType": "text/html" }
], ],
"transformOptions": [ "transformOptions": [
"alfmarkdownOptions" "alfmarkdownOptions"

View File

@ -7,7 +7,29 @@ transform:
location: classpath:this_engine_config.json location: classpath:this_engine_config.json
alfmarkdown: alfmarkdown:
version: ${project.version} version: ${project.version}
commonmark:
defaultExtensions:
- ImageAttributesExtension
- TaskListItemsExtension
- TablesExtension
- InsExtension
flexmark:
defaultProfile: github
defaultExtensions:
- AdmonitionExtension
- DefinitionExtension
- EmojiExtension
- EnumeratedReferenceExtension
- FootnoteExtension
- InsExtension
- MediaTagsExtension
- SubscriptExtension
- SuperscriptExtension
- TablesExtension
- TaskListExtension
- TypographicExtension
- YouTubeLinkExtension
logging: logging:
level: level:
com.inteligr8.alfresco.module: ${LOG_LEVEL:trace} com.inteligr8.alfresco.module.alfmarkdown: ${LOG_LEVEL:trace}