Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git

Call out to ConTeXt for typesetting

Author DaveJarvis <email>
Date 2021-03-30 23:36:57 GMT-0700
Commit f9c1a4878dfff7cce102f953e5f341f58a4e8346
Parent 7b2f511
Delta 262 lines added, 130 lines removed, 132-line increase
src/main/java/com/keenwrite/MainPane.java
public ProcessorContext createProcessorContext(
- final File exportPath, final ExportFormat format ) {
+ final Path exportPath, final ExportFormat format ) {
final var editor = getActiveTextEditor();
return createProcessorContext(
- editor.getPath(), editor.getCaret(), exportPath, format );
+ editor.getPath(), exportPath, format, editor.getCaret() );
}
private ProcessorContext createProcessorContext(
final Path path, final Caret caret ) {
- return createProcessorContext( path, caret, null, ExportFormat.NONE );
+ return createProcessorContext( path, null, ExportFormat.NONE, caret );
}
/**
* @param path Used by {@link ProcessorFactory} to determine
* {@link Processor} type to create based on file type.
- * @param caret Used by {@link CaretExtension} to add ID attribute into
- * preview document for scrollbar synchronization.
* @param exportPath Used when exporting to a PDF file (binary).
* @param format Used when processors export to a new text format.
+ * @param caret Used by {@link CaretExtension} to add ID attribute into
+ * preview document for scrollbar synchronization.
* @return A new {@link ProcessorContext} to use when creating an instance of
* {@link Processor}.
*/
private ProcessorContext createProcessorContext(
- final Path path, final Caret caret,
- final File exportPath, final ExportFormat format ) {
+ final Path path, final Path exportPath, final ExportFormat format,
+ final Caret caret ) {
return new ProcessorContext(
- mPreview, mResolvedMap, path, caret, exportPath, format, mWorkspace
+ mPreview, mResolvedMap, path, exportPath, format, mWorkspace, caret
);
}
src/main/java/com/keenwrite/io/MediaType.java
TEXT_R_MARKDOWN( TEXT, "R+markdown" ),
TEXT_R_XML( TEXT, "R+xml" ),
+ TEXT_XHTML( TEXT, "xhtml+xml" ),
+ TEXT_XML( TEXT, "xml" ),
TEXT_YAML( TEXT, "yaml" ),
*/
public boolean isSvg() {
- return this == IMAGE_SVG_XML;
+ // Kroki serves HTTP HEAD requests back as text/plain for SVG images.
+ return this == IMAGE_SVG_XML || this == TEXT_PLAIN;
}
src/main/java/com/keenwrite/io/MediaTypeExtension.java
MEDIA_TEXT_R_MARKDOWN( TEXT_R_MARKDOWN, of( "Rmd" ) ),
MEDIA_TEXT_R_XML( TEXT_R_XML, of( "Rxml" ) ),
+ MEDIA_TEXT_XHTML( TEXT_XHTML, of( "xhtml" ) ),
+ MEDIA_TEXT_XML( TEXT_XML ),
MEDIA_TEXT_YAML( TEXT_YAML, of( "yaml", "yml" ) ),
src/main/java/com/keenwrite/preferences/PreferencesController.java
localeProperty( KEY_LANGUAGE_LOCALE ) )
)
+ ),
+ Category.of(
+ get( KEY_TYPESET ),
+ Group.of(
+ get( KEY_TYPESET_CONTEXT ),
+ Setting.of( label( KEY_TYPESET_CONTEXT_PATH ) ),
+ Setting.of( title( KEY_TYPESET_CONTEXT_PATH ),
+ stringProperty( KEY_TYPESET_CONTEXT_PATH ) ),
+ Setting.of( label( KEY_TYPESET_CONTEXT_ENV ) ),
+ Setting.of( title( KEY_TYPESET_CONTEXT_ENV ),
+ stringProperty( KEY_TYPESET_CONTEXT_ENV ) )
+ )
)
).instantPersistent( false ).dialogIcon( ICON_DIALOG );
src/main/java/com/keenwrite/preferences/Workspace.java
entry( KEY_UI_THEME_CUSTOM, asFileProperty( THEME_CUSTOM_DEFAULT ) ),
- entry( KEY_LANGUAGE_LOCALE, asLocaleProperty( LOCALE_DEFAULT ) )
- );
+ entry( KEY_LANGUAGE_LOCALE, asLocaleProperty( LOCALE_DEFAULT ) ),
+
+ entry( KEY_TYPESET_CONTEXT_PATH, asStringProperty( USER_DIRECTORY.toString() ) ),
+ entry( KEY_TYPESET_CONTEXT_ENV, asStringProperty( "" ) )
+ );
//@formatter:on
src/main/java/com/keenwrite/preferences/WorkspaceKeys.java
public static final Key KEY_UI_THEME_CUSTOM = key( KEY_UI_THEME, "custom" );
-// public static final Key KEY_UI_THEME_CUSTOM = key( KEY_UI_THEME, "custom" );
-// public static final Key KEY_UI_THEME_CUSTOM_FONT = key( KEY_UI_THEME_CUSTOM, "font" );
-// public static final Key KEY_UI_THEME_CUSTOM_FONT_SIZE = key( KEY_UI_THEME_CUSTOM_FONT, "size" );
-// public static final Key KEY_UI_THEME_CUSTOM_COLOURS = key( KEY_UI_THEME_CUSTOM, "colours" );
-// public static final Key KEY_UI_THEME_CUSTOM_COLOURS_BASE = key( KEY_UI_THEME_CUSTOM_COLOURS, "base" );
-// public static final Key KEY_UI_THEME_CUSTOM_COLOURS_BG = key( KEY_UI_THEME_CUSTOM_COLOURS, "background" );
-// public static final Key KEY_UI_THEME_CUSTOM_COLOURS_CONTROLS = key( KEY_UI_THEME_CUSTOM_COLOURS, "controls" );
-// public static final Key KEY_UI_THEME_CUSTOM_COLOURS_ROW1 = key( KEY_UI_THEME_CUSTOM_COLOURS, "row" );
-// public static final Key KEY_UI_THEME_CUSTOM_COLOURS_ROW2 = key( KEY_UI_THEME_CUSTOM_COLOURS, "row" );
-// public static final Key KEY_UI_THEME_CUSTOM_COLOURS_FG = key( KEY_UI_THEME_CUSTOM_COLOURS, "foreground" );
-// public static final Key KEY_UI_THEME_CUSTOM_COLOURS_FG_LIGHT = key( KEY_UI_THEME_CUSTOM_COLOURS_FG, "light" );
-// public static final Key KEY_UI_THEME_CUSTOM_COLOURS_FG_MEDIUM = key( KEY_UI_THEME_CUSTOM_COLOURS_FG, "medium" );
-// public static final Key KEY_UI_THEME_CUSTOM_COLOURS_FG_DARK = key( KEY_UI_THEME_CUSTOM_COLOURS_FG, "dark" );
-// public static final Key KEY_UI_THEME_CUSTOM_COLOURS_ACCENT = key( KEY_UI_THEME_CUSTOM_COLOURS, "accent" );
-// public static final Key KEY_UI_THEME_CUSTOM_COLOURS_UNFOCUSED = key( KEY_UI_THEME_CUSTOM_COLOURS, "unfocused" );
-// public static final Key KEY_UI_THEME_CUSTOM_COLOURS_SCROLLBAR = key( KEY_UI_THEME_CUSTOM_COLOURS, "scrollbar" );
-// public static final Key KEY_UI_THEME_CUSTOM_COLOURS_SCROLLBAR_BUTTON = key( KEY_UI_THEME_CUSTOM_COLOURS_SCROLLBAR, "button" );
-// public static final Key KEY_UI_THEME_CUSTOM_COLOURS_SCROLLBAR_BUTTON_RELEASED = key( KEY_UI_THEME_CUSTOM_COLOURS_SCROLLBAR_BUTTON, "released" );
-// public static final Key KEY_UI_THEME_CUSTOM_COLOURS_SCROLLBAR_BUTTON_PRESSED = key( KEY_UI_THEME_CUSTOM_COLOURS_SCROLLBAR_BUTTON, "pressed" );
-// public static final Key KEY_UI_THEME_CUSTOM_COLOURS_SCROLLBAR_BUTTON_HOVER = key( KEY_UI_THEME_CUSTOM_COLOURS_SCROLLBAR_BUTTON, "hover" );
-
public static final Key KEY_LANGUAGE = key( KEY_ROOT, "language" );
public static final Key KEY_LANGUAGE_LOCALE = key( KEY_LANGUAGE, "locale" );
+
+ public static final Key KEY_TYPESET = key( KEY_ROOT, "typeset" );
+ public static final Key KEY_TYPESET_CONTEXT = key( KEY_TYPESET, "context" );
+ public static final Key KEY_TYPESET_CONTEXT_PATH = key( KEY_TYPESET_CONTEXT, "path" );
+ public static final Key KEY_TYPESET_CONTEXT_ENV = key( KEY_TYPESET_CONTEXT, "environment" );
//@formatter:on
/**
- *
+ * Only for constants, do not instantiate.
*/
private WorkspaceKeys() { }
src/main/java/com/keenwrite/processors/PdfProcessor.java
import com.keenwrite.typesetting.Typesetter;
-import java.io.File;
-import java.io.IOException;
-
import static com.keenwrite.events.StatusEvent.clue;
-import static com.keenwrite.io.MediaType.APP_PDF;
+import static com.keenwrite.io.MediaType.TEXT_XML;
import static com.keenwrite.util.FileUtils.createTemporaryFile;
+import static java.nio.file.Files.writeString;
/**
* Responsible for using a typesetting engine to convert an XHTML document
* into a PDF file.
*/
public final class PdfProcessor extends ExecutorProcessor<String> {
- private static final Typesetter sTypesetter = new Typesetter();
- private final File mExportPath;
+ private final ProcessorContext mContext;
- public PdfProcessor( final File exportPath ) {
- assert exportPath != null;
- mExportPath = exportPath;
+ public PdfProcessor( final ProcessorContext context ) {
+ assert context != null;
+ mContext = context;
}
public String apply( final String xhtml ) {
try {
- final var document = createTemporaryFile( APP_PDF );
- sTypesetter.typeset( document, mExportPath );
- } catch( final IOException ex ) {
+ final var sTypesetter = new Typesetter( mContext.getWorkspace() );
+ final var document = createTemporaryFile( TEXT_XML );
+ final var exportPath = mContext.getExportPath();
+ sTypesetter.typeset( writeString( document, xhtml ), exportPath );
+ } catch( final Exception ex ) {
clue( ex );
}
+ // Do not continue processing (the document was typeset into a binary).
return null;
}
src/main/java/com/keenwrite/processors/ProcessorContext.java
import com.keenwrite.preview.HtmlPreview;
-import java.io.File;
import java.nio.file.Path;
import java.util.Map;
private final Map<String, String> mResolvedMap;
private final Path mDocumentPath;
+ private final Path mExportPath;
private final Caret mCaret;
- private final File mExportPath;
private final ExportFormat mExportFormat;
private final Workspace mWorkspace;
* @param resolvedMap Fully expanded interpolated strings.
* @param documentPath Path to the document to process.
- * @param caret Location of the caret in the edited document, which is
- * used to synchronize the scrollbars.
* @param exportPath Fully qualified filename to use when exporting.
* @param exportFormat Indicate configuration options for export format.
* @param workspace Persistent user preferences settings.
+ * @param caret Location of the caret in the edited document, which is
+ * used to synchronize the scrollbars.
*/
public ProcessorContext(
final HtmlPreview htmlPreview,
final Map<String, String> resolvedMap,
final Path documentPath,
- final Caret caret,
- final File exportPath,
+ final Path exportPath,
final ExportFormat exportFormat,
- final Workspace workspace ) {
+ final Workspace workspace,
+ final Caret caret ) {
assert htmlPreview != null;
assert resolvedMap != null;
assert documentPath != null;
- assert caret != null;
assert exportFormat != null;
assert workspace != null;
+ assert caret != null;
mHtmlPreview = htmlPreview;
* @return Full path to a file name.
*/
- public File getExportPath() {
+ public Path getExportPath() {
return mExportPath;
}
src/main/java/com/keenwrite/processors/ProcessorFactory.java
private Processor<String> createPdfProcessor(
final ProcessorContext context ) {
- final var pdfp = new PdfProcessor( context.getExportPath() );
+ final var pdfp = new PdfProcessor( context );
return createXhtmlProcessor( pdfp, context );
}
src/main/java/com/keenwrite/typesetting/Typesetter.java
package com.keenwrite.typesetting;
-import java.io.File;
+import com.keenwrite.preferences.Key;
+import com.keenwrite.preferences.Workspace;
+
+import java.io.IOException;
import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import static com.keenwrite.Constants.DEFAULT_DIRECTORY;
+import static com.keenwrite.Messages.get;
+import static com.keenwrite.events.StatusEvent.clue;
+import static com.keenwrite.preferences.WorkspaceKeys.KEY_TYPESET_CONTEXT_ENV;
+import static com.keenwrite.preferences.WorkspaceKeys.KEY_TYPESET_CONTEXT_PATH;
import static com.keenwrite.util.FileUtils.canExecute;
+import static java.lang.String.format;
+import static java.lang.System.currentTimeMillis;
+import static java.util.concurrent.Executors.newFixedThreadPool;
+import static java.util.concurrent.TimeUnit.*;
/**
* Represents the executable responsible for typesetting text. This will
* construct suitable command-line arguments to invoke the typesetting engine.
*/
public class Typesetter {
private static final String TYPESETTER = "context";
- public Typesetter() {
- }
+ private static final ExecutorService sService = newFixedThreadPool( 5 );
- public boolean isInstalled() {
- return canExecute( TYPESETTER );
+ private final Workspace mWorkspace;
+
+ /**
+ * Creates a new {@link Typesetter} instance capable of configuring the
+ * typesetter used to generate a typeset document.
+ */
+ public Typesetter( final Workspace workspace ) {
+ mWorkspace = workspace;
}
/**
* This will typeset the document using a new process.
*
* @param input The input document to typeset.
* @param output Path to the finished typeset document.
*/
- public void typeset( final Path input, final File output ) {
+ public void typeset( final Path input, final Path output )
+ throws IOException {
+ if( isInstalled() ) {
+ sService.submit( new TypesetTask( input, output ) );
+ }
+ }
+
+ /**
+ * Launches a task to typeset a document.
+ */
+ public class TypesetTask implements Callable<Integer> {
+ private final List<String> mArgs = new ArrayList<>();
+
+ /**
+ * Working directory must be set because ConTeXt cannot write the
+ * result to an arbitrary location.
+ */
+ private final Path mDirectory;
+
+ /**
+ * Fully qualified destination file name.
+ */
+ private final Path mOutput;
+
+ public TypesetTask( final Path input, final Path output ) {
+ final var filename = output.getFileName();
+ final var parentDir = output.getParent();
+ mDirectory = (parentDir == null ? DEFAULT_DIRECTORY : parentDir);
+ mOutput = output;
+
+ final var paths = getProperty( KEY_TYPESET_CONTEXT_PATH );
+ final var envs = getProperty( KEY_TYPESET_CONTEXT_ENV );
+
+ mArgs.add( TYPESETTER );
+ mArgs.add( "--batchmode" );
+ mArgs.add( "--purgeall" );
+ mArgs.add( "--path='" + paths + "'" );
+ mArgs.add( "--environment='" + envs + "'" );
+ mArgs.add( "--result='" + filename + "'" );
+ mArgs.add( input.toString() );
+ }
+
+ @Override
+ public Integer call() throws Exception {
+ final var elapsed = currentTimeMillis();
+ final var output = mOutput.toString();
+ clue( get( "Main.status.typeset.began", output ) );
+
+ final var builder = new ProcessBuilder( mArgs );
+ builder.directory( mDirectory.toFile() );
+ final var process = builder.start();
+ process.waitFor();
+
+ final var code = process.exitValue();
+ final var time = asElapsed( currentTimeMillis() - elapsed );
+ clue(
+ code == 0
+ ? get( "Main.status.typeset.ended.success", output, time )
+ : get( "Main.status.typeset.ended.failure", output, time, code )
+ );
+
+ return code;
+ }
+ }
+
+ private String getProperty( final Key key ) {
+ return mWorkspace.stringProperty( key ).toString();
+ }
+
+ /**
+ * Helps ensure that the typesetting software can be run.
+ *
+ * @return {@code true} when the typesetting software is available.
+ */
+ private boolean isInstalled() {
+ return canExecute( TYPESETTER );
+ }
+
+ /**
+ * Converts an elapsed time to a human-readable format (hours, minutes,
+ * seconds, and milliseconds).
+ *
+ * @param elapsed An elapsed time, in milliseconds.
+ * @return Human-readable elapsed time.
+ */
+ private static String asElapsed( final long elapsed ) {
+ final var hours = MILLISECONDS.toHours( elapsed );
+ final var eHours = elapsed - HOURS.toMillis( hours );
+ final var minutes = MILLISECONDS.toMinutes( eHours );
+ final var eMinutes = eHours - MINUTES.toMillis( minutes );
+ final var seconds = MILLISECONDS.toSeconds( eMinutes );
+ final var eSeconds = eMinutes - SECONDS.toMillis( seconds );
+ final var milliseconds = MILLISECONDS.toMillis( eSeconds );
+ return format( "%02d:%02d:%02d.%03d",
+ hours, minutes, seconds, milliseconds );
}
}
src/main/java/com/keenwrite/ui/actions/ApplicationActions.java
selection.ifPresent( ( file ) -> {
final var doc = editor.getText();
- final var context = main.createProcessorContext( file, format );
+ final var context = main.createProcessorContext( file.toPath(), format );
final var chain = createProcessors( context );
final var export = chain.apply( doc );
try {
// Processors can export in binary formats that are incompatible with
// Java language String objects. In such cases, the processor will
// return the null sentinel to signal no further processing is needed.
if( export != null ) {
writeString( file.toPath(), export );
- }
- clue( get( "Main.status.export.success", file.toString() ) );
+ // Binary formats must notify users of success independently.
+ clue( get( "Main.status.export.success", file.toString() ) );
+ }
} catch( final Exception ex ) {
clue( ex );
src/main/resources/com/keenwrite/messages.properties
# ########################################################################
-# Menu Bar
-# ########################################################################
-
-Main.menu.file=_File
-Main.menu.edit=_Edit
-Main.menu.insert=_Insert
-Main.menu.format=Forma_t
-Main.menu.definition=_Variable
-Main.menu.view=Vie_w
-Main.menu.help=_Help
-
-# ########################################################################
-# Detachable Tabs
-# ########################################################################
-
-# {0} is the application title; {1} is a unique window ID.
-Detach.tab.title={0} - {1}
-
-# ########################################################################
-# Status Bar
-# ########################################################################
-
-Main.status.text.offset=offset
-Main.status.line=Line {0} of {1}, ${Main.status.text.offset} {2}
-Main.status.state.default=OK
-Main.status.export.success=Saved as {0}
-
-Main.status.error.bootstrap.eval=Note: Bootstrap variable of ''{0}'' not found
-
-Main.status.error.parse={0} (near ${Main.status.text.offset} {1})
-Main.status.error.def.blank=Move the caret to a word before inserting a variable
-Main.status.error.def.empty=Create a variable before inserting one
-Main.status.error.def.missing=No variable value found for ''{0}''
-Main.status.error.r=Error with [{0}...]: {1}
-Main.status.error.file.missing=Not found: {0}
-
-Main.status.error.messages.recursion=Lookup depth exceeded, check for loops in ''{0}''
-Main.status.error.messages.syntax=Missing ''}'' in ''{0}''
-
-Main.status.error.undo=Cannot undo; beginning of undo history reached
-Main.status.error.redo=Cannot redo; end of redo history reached
-
-Main.status.image.request.init=Initializing HTTP request
-Main.status.image.request.fetch=Requesting content type from {0}
-Main.status.image.request.success=Determined content type ''{0}''
-Main.status.image.request.error.media=No media type for ''{0}''
-Main.status.image.request.error.cert=Could not accept certificate for ''{0}''
-
-Main.status.font.search.missing=No font name starting with ''{0}'' was found
-
-# ########################################################################
-# Search Bar
-# ########################################################################
-
-Main.search.stop.tooltip=Close search bar
-Main.search.stop.icon=CLOSE
-Main.search.next.tooltip=Find next match
-Main.search.next.icon=CHEVRON_DOWN
-Main.search.prev.tooltip=Find previous match
-Main.search.prev.icon=CHEVRON_UP
-Main.search.find.tooltip=Search document for text
-Main.search.find.icon=SEARCH
-Main.search.match.none=No matches
-Main.search.match.some={0} of {1} matches
-
-# ########################################################################
# Workspace preferences
# ########################################################################
workspace.language.locale.desc=Language for application and HTML export.
workspace.language.locale.title=Locale
+
+workspace.typeset=Typesetting
+workspace.typeset.context=ConTeXt
+workspace.typeset.context.path=Paths
+workspace.typeset.context.path.desc=Comma-separated directories containing support files
+workspace.typeset.context.path.title=Directories
+workspace.typeset.context.environment=Environments
+workspace.typeset.context.environment.desc=Comma-separated environment file names
+workspace.typeset.context.environment.title=Files
+
+# ########################################################################
+# Menu Bar
+# ########################################################################
+
+Main.menu.file=_File
+Main.menu.edit=_Edit
+Main.menu.insert=_Insert
+Main.menu.format=Forma_t
+Main.menu.definition=_Variable
+Main.menu.view=Vie_w
+Main.menu.help=_Help
+
+# ########################################################################
+# Detachable Tabs
+# ########################################################################
+
+# {0} is the application title; {1} is a unique window ID.
+Detach.tab.title={0} - {1}
+
+# ########################################################################
+# Status Bar
+# ########################################################################
+
+Main.status.text.offset=offset
+Main.status.line=Line {0} of {1}, ${Main.status.text.offset} {2}
+Main.status.state.default=OK
+Main.status.export.success=Saved as ''{0}''
+
+Main.status.error.bootstrap.eval=Note: Bootstrap variable of ''{0}'' not found
+
+Main.status.error.parse={0} (near ${Main.status.text.offset} {1})
+Main.status.error.def.blank=Move the caret to a word before inserting a variable
+Main.status.error.def.empty=Create a variable before inserting one
+Main.status.error.def.missing=No variable value found for ''{0}''
+Main.status.error.r=Error with [{0}...]: {1}
+Main.status.error.file.missing=Not found: ''{0}''
+
+Main.status.error.messages.recursion=Lookup depth exceeded, check for loops in ''{0}''
+Main.status.error.messages.syntax=Missing ''}'' in ''{0}''
+
+Main.status.error.undo=Cannot undo; beginning of undo history reached
+Main.status.error.redo=Cannot redo; end of redo history reached
+
+Main.status.image.request.init=Initializing HTTP request
+Main.status.image.request.fetch=Requesting content type from ''{0}''
+Main.status.image.request.success=Determined content type ''{0}''
+Main.status.image.request.error.media=No media type for ''{0}''
+Main.status.image.request.error.cert=Could not accept certificate for ''{0}''
+
+Main.status.typeset.began=Started typesetting ''{0}''
+Main.status.typeset.ended.success=Finished typesetting ''{0}'' ({1} elapsed)
+Main.status.typeset.ended.failure=Failed to typeset ''{0}'' ({1} elapsed); exit code: {2}
+
+Main.status.font.search.missing=No font name starting with ''{0}'' was found
+
+# ########################################################################
+# Search Bar
+# ########################################################################
+
+Main.search.stop.tooltip=Close search bar
+Main.search.stop.icon=CLOSE
+Main.search.next.tooltip=Find next match
+Main.search.next.icon=CHEVRON_DOWN
+Main.search.prev.tooltip=Find previous match
+Main.search.prev.icon=CHEVRON_UP
+Main.search.find.tooltip=Search document for text
+Main.search.find.icon=SEARCH
+Main.search.match.none=No matches
+Main.search.match.some={0} of {1} matches
# ########################################################################