| Author | DaveJarvis <email> |
|---|---|
| Date | 2020-11-08 22:00:22 GMT-0800 |
| Commit | 6602414a70e2c7bc597d396684e085d750b4edcb |
| Parent | 30f05e8 |
| Delta | 102 lines added, 101 lines removed, 1-line increase |
| * Responsible for parsing a Markdown document and rendering it as HTML. | ||
| */ | ||
| -public class MarkdownProcessor extends AbstractProcessor<String> { | ||
| +public class MarkdownProcessor extends ExecutorProcessor<String> { | ||
| private final IParse mParser; |
| * {@code $variable$}. | ||
| */ | ||
| -public class DefinitionProcessor extends AbstractProcessor<String> { | ||
| +public class DefinitionProcessor extends ExecutorProcessor<String> { | ||
| private final Map<String, String> mDefinitions; |
| +package com.keenwrite.processors; | ||
| + | ||
| +import java.util.Optional; | ||
| +import java.util.concurrent.atomic.AtomicReference; | ||
| + | ||
| +/** | ||
| + * Responsible for transforming data through a variety of chained handlers. | ||
| + * | ||
| + * @param <T> The data type to process. | ||
| + */ | ||
| +public class ExecutorProcessor<T> implements Processor<T> { | ||
| + | ||
| + /** | ||
| + * The next link in the processing chain. | ||
| + */ | ||
| + private final Processor<T> mNext; | ||
| + | ||
| + protected ExecutorProcessor() { | ||
| + this( null ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Constructs a new processor having a given successor. | ||
| + * | ||
| + * @param successor The next processor in the chain. | ||
| + */ | ||
| + public ExecutorProcessor( final Processor<T> successor ) { | ||
| + mNext = successor; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Calls every link in the chain to process the given data. | ||
| + * | ||
| + * @param data The data to transform. | ||
| + * @return The data after processing by every link in the chain. | ||
| + */ | ||
| + @Override | ||
| + public T apply( final T data ) { | ||
| + // Avoid infinite recursion. | ||
| + Optional<Processor<T>> handler = next(); | ||
| + final var result = new AtomicReference<>( data ); | ||
| + | ||
| + while( handler.isPresent() ) { | ||
| + handler = handler.flatMap( p -> { | ||
| + result.set( p.apply( result.get() ) ); | ||
| + return p.next(); | ||
| + } ); | ||
| + } | ||
| + | ||
| + return result.get(); | ||
| + } | ||
| + | ||
| + @Override | ||
| + public Optional<Processor<T>> next() { | ||
| + return Optional.ofNullable( mNext ); | ||
| + } | ||
| +} | ||
| * chain. | ||
| */ | ||
| -public class HtmlPreviewProcessor extends AbstractProcessor<String> { | ||
| +public class HtmlPreviewProcessor extends ExecutorProcessor<String> { | ||
| // There is only one preview panel. |
| * end of a processing chain when no more processing is required. | ||
| */ | ||
| -public class IdentityProcessor extends AbstractProcessor<String> { | ||
| +public class IdentityProcessor extends ExecutorProcessor<String> { | ||
| public static final IdentityProcessor INSTANCE = new IdentityProcessor(); | ||
| * element. | ||
| */ | ||
| -public class PreformattedProcessor extends AbstractProcessor<String> { | ||
| +public class PreformattedProcessor extends ExecutorProcessor<String> { | ||
| /** |
| package com.keenwrite.processors; | ||
| +import java.util.Optional; | ||
| import java.util.function.UnaryOperator; | ||
| /** | ||
| * Responsible for processing documents from one known format to another. | ||
| * Processes the given content providing a transformation from one document | ||
| - * format into another. For example, this could convert from XML to text using | ||
| - * an XSLT processor, or from markdown to HTML. | ||
| + * format into another. For example, this could convert Markdown to HTML. | ||
| * | ||
| - * @param <T> The type of processor to create. | ||
| + * @param <T> The data type to process. | ||
| */ | ||
| public interface Processor<T> extends UnaryOperator<T> { | ||
| /** | ||
| - * Adds a document processor to call after this processor finishes processing | ||
| - * the document given to the process method. | ||
| + * Returns the next link in the processor chain. | ||
| * | ||
| - * @return The processor that should transform the document after this | ||
| - * instance has finished processing, or {@code null} if this is the last | ||
| - * processor in the chain. | ||
| + * @return The processor intended to transform the data after this instance | ||
| + * has finished processing, or {@link Optional#empty} if this is the last | ||
| + * link in the chain. | ||
| */ | ||
| - default Processor<T> next() { | ||
| - return null; | ||
| + default Optional<Processor<T>> next() { | ||
| + return Optional.empty(); | ||
| } | ||
| } |
| private Processor<String> createProcessor() { | ||
| - final ProcessorContext context = getProcessorContext(); | ||
| + final var context = getProcessorContext(); | ||
| // If the content is not to be exported, then the successor processor | ||
| // to SVG. Without conversion would require client-side rendering of | ||
| // math (such as using the JavaScript-based KaTeX engine). | ||
| - final Processor<String> successor = context.isExportFormat( NONE ) | ||
| + final var successor = context.isExportFormat( NONE ) | ||
| ? createHtmlPreviewProcessor() | ||
| : createIdentityProcessor(); | ||
| - return switch( context.getFileType() ) { | ||
| + final var processor = switch( context.getFileType() ) { | ||
| case RMARKDOWN -> createRProcessor( successor ); | ||
| case SOURCE -> createMarkdownProcessor( successor ); | ||
| case RXML -> createRXMLProcessor( successor ); | ||
| case XML -> createXMLProcessor( successor ); | ||
| default -> createPreformattedProcessor( successor ); | ||
| }; | ||
| + | ||
| + return new ExecutorProcessor<>( processor ); | ||
| } | ||
| final ProcessorContext context ) { | ||
| return new ProcessorFactory( context ).createProcessor(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Executes the processing chain, operating on the given string. | ||
| - * | ||
| - * @param handler The first processor in the chain to call. | ||
| - * @param text The initial value of the text to process. | ||
| - * @return The final value of the text that was processed by the chain. | ||
| - */ | ||
| - public static String processChain( Processor<String> handler, String text ) { | ||
| - while( handler != null && text != null ) { | ||
| - text = handler.apply( text ); | ||
| - handler = handler.next(); | ||
| - } | ||
| - | ||
| - return text; | ||
| } | ||
| import javax.xml.stream.XMLStreamException; | ||
| import javax.xml.stream.events.ProcessingInstruction; | ||
| -import javax.xml.stream.events.XMLEvent; | ||
| import javax.xml.transform.*; | ||
| import javax.xml.transform.stream.StreamResult; | ||
| import javax.xml.transform.stream.StreamSource; | ||
| -import java.io.File; | ||
| import java.io.Reader; | ||
| import java.io.StringReader; | ||
| * </p> | ||
| */ | ||
| -public class XmlProcessor extends AbstractProcessor<String> | ||
| +public class XmlProcessor extends ExecutorProcessor<String> | ||
| implements ErrorListener { | ||
| - private final Snitch snitch = Services.load( Snitch.class ); | ||
| + private final Snitch mSnitch = Services.load( Snitch.class ); | ||
| - private XMLInputFactory xmlInputFactory; | ||
| - private TransformerFactory transformerFactory; | ||
| - private Transformer transformer; | ||
| + private final XMLInputFactory mXmlInputFactory = | ||
| + XMLInputFactory.newInstance(); | ||
| + private final TransformerFactory mTransformerFactory = | ||
| + new TransformerFactoryImpl(); | ||
| + private Transformer mTransformer; | ||
| - private Path path; | ||
| + private final Path mPath; | ||
| /** | ||
| final ProcessorContext context ) { | ||
| super( successor ); | ||
| - setPath( context.getPath() ); | ||
| + mPath = context.getPath(); | ||
| + | ||
| + // Bubble problems up to the user interface, rather than standard error. | ||
| + mTransformerFactory.setErrorListener( this ); | ||
| } | ||
| // Listen for external file modification events. | ||
| - getSnitch().listen( xsl ); | ||
| + mSnitch.listen( xsl ); | ||
| getTransformer( xsl ).transform( | ||
| * transformer. | ||
| */ | ||
| - private Transformer getTransformer( final Path xsl ) | ||
| + private synchronized Transformer getTransformer( final Path xsl ) | ||
| throws TransformerConfigurationException { | ||
| - if( this.transformer == null ) { | ||
| - this.transformer = createTransformer( xsl ); | ||
| + if( mTransformer == null ) { | ||
| + mTransformer = createTransformer( xsl ); | ||
| } | ||
| - return this.transformer; | ||
| + return mTransformer; | ||
| } | ||
| protected Transformer createTransformer( final Path xsl ) | ||
| throws TransformerConfigurationException { | ||
| - final Source xslt = new StreamSource( xsl.toFile() ); | ||
| + final var xslt = new StreamSource( xsl.toFile() ); | ||
| return getTransformerFactory().newTransformer( xslt ); | ||
| } | ||
| private Path getXslPath( final String filename ) { | ||
| - final Path xmlPath = getPath(); | ||
| - final File xmlDirectory = xmlPath.toFile().getParentFile(); | ||
| + final var xmlDirectory = mPath.toFile().getParentFile(); | ||
| return Paths.get( xmlDirectory.getPath(), filename ); | ||
| private String getXsltFilename( final String xml ) | ||
| throws XMLStreamException, XPathException { | ||
| - | ||
| String result = ""; | ||
| try( final StringReader sr = new StringReader( xml ) ) { | ||
| + final XMLEventReader reader = createXmlEventReader( sr ); | ||
| boolean found = false; | ||
| int count = 0; | ||
| - final XMLEventReader reader = createXMLEventReader( sr ); | ||
| // If the processing instruction wasn't found in the first 10 lines, | ||
| // fail fast. This should iterate twice through the loop. | ||
| while( !found && reader.hasNext() && count++ < 10 ) { | ||
| - final XMLEvent event = reader.nextEvent(); | ||
| + final var event = reader.nextEvent(); | ||
| if( event.isProcessingInstruction() ) { | ||
| - final ProcessingInstruction pi = (ProcessingInstruction) event; | ||
| - final String target = pi.getTarget(); | ||
| + final var pi = (ProcessingInstruction) event; | ||
| + final var target = pi.getTarget(); | ||
| if( "xml-stylesheet".equalsIgnoreCase( target ) ) { | ||
| } | ||
| - private XMLEventReader createXMLEventReader( final Reader reader ) | ||
| + private XMLEventReader createXmlEventReader( final Reader reader ) | ||
| throws XMLStreamException { | ||
| - return getXMLInputFactory().createXMLEventReader( reader ); | ||
| - } | ||
| - | ||
| - private synchronized XMLInputFactory getXMLInputFactory() { | ||
| - if( this.xmlInputFactory == null ) { | ||
| - this.xmlInputFactory = createXMLInputFactory(); | ||
| - } | ||
| - | ||
| - return this.xmlInputFactory; | ||
| - } | ||
| - | ||
| - private XMLInputFactory createXMLInputFactory() { | ||
| - return XMLInputFactory.newInstance(); | ||
| + return mXmlInputFactory.createXMLEventReader( reader ); | ||
| } | ||
| private synchronized TransformerFactory getTransformerFactory() { | ||
| - if( this.transformerFactory == null ) { | ||
| - this.transformerFactory = createTransformerFactory(); | ||
| - } | ||
| - | ||
| - return this.transformerFactory; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns a high-performance XSLT 2 transformation engine. | ||
| - * | ||
| - * @return An XSL transforming engine. | ||
| - */ | ||
| - private TransformerFactory createTransformerFactory() { | ||
| - final TransformerFactory factory = new TransformerFactoryImpl(); | ||
| - | ||
| - // Bubble problems up to the user interface, rather than standard error. | ||
| - factory.setErrorListener( this ); | ||
| - | ||
| - return factory; | ||
| + return mTransformerFactory; | ||
| } | ||
| public void fatalError( final TransformerException ex ) { | ||
| throw new RuntimeException( ex ); | ||
| - } | ||
| - | ||
| - private void setPath( final Path path ) { | ||
| - this.path = path; | ||
| - } | ||
| - | ||
| - private Path getPath() { | ||
| - return this.path; | ||
| - } | ||
| - | ||
| - private Snitch getSnitch() { | ||
| - return this.snitch; | ||
| } | ||
| } | ||