Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
src/main/java/com/keenwrite/MainPane.java
() -> w.getString( KEY_TYPESET_MODES_ENABLED ) )
.with( Mutator::setCurlQuotes,
- () -> w.getBoolean( KEY_TYPESET_TYPOGRAPHY_QUOTES ) )
+ () -> w.listProperty( KEY_TYPESET_TYPOGRAPHY_QUOTES ).get() )
.with( Mutator::setAutoRemove,
() -> w.getBoolean( KEY_TYPESET_CONTEXT_CLEAN ) );
src/main/java/com/keenwrite/MainScene.java
}
- MenuBar getMenuBar() {
- return mMenuBar;
- }
-
public StatusBar getStatusBar() {return mStatusBar;}
private void initStylesheets( final Scene scene, final Workspace workspace ) {
- final var internal = workspace.skinProperty( KEY_UI_SKIN_SELECTION );
+ final var internal = workspace.listProperty( KEY_UI_SKIN_SELECTION );
final var external = workspace.fileProperty( KEY_UI_SKIN_CUSTOM );
final var inSkin = internal.get();
src/main/java/com/keenwrite/cmdline/Arguments.java
names = { "--curl-quotes" },
description =
- "Replace straight quotes with curly quotes (${DEFAULT-VALUE})",
- defaultValue = "true"
+ "Encode quotation marks (regular, modifier, apos, aposhex, quote, quotehex)",
+ defaultValue = "regular"
)
- private boolean mCurlQuotes;
+ private String mCurlQuotes;
@CommandLine.Option(
src/main/java/com/keenwrite/constants/Constants.java
/**
+ * The default apostrophe to use when exporting.
+ */
+ public static final String APOS_DEFAULT = "apos";
+
+ /**
* Prevent instantiation.
*/
src/main/java/com/keenwrite/dom/DocumentParser.java
package com.keenwrite.dom;
-import org.w3c.dom.*;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerException;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.stream.StreamResult;
-import javax.xml.xpath.XPath;
-import javax.xml.xpath.XPathExpression;
-import javax.xml.xpath.XPathExpressionException;
-import javax.xml.xpath.XPathFactory;
-import java.io.*;
-import java.nio.file.Path;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.function.Consumer;
-
-import static com.keenwrite.events.StatusEvent.clue;
-import static com.keenwrite.io.SysFile.toFile;
-import static java.nio.charset.StandardCharsets.UTF_16;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.nio.file.Files.write;
-import static javax.xml.transform.OutputKeys.*;
-import static javax.xml.xpath.XPathConstants.NODESET;
-
-/**
- * Responsible for initializing an XML parser.
- */
-public class DocumentParser {
- private static final String LOAD_EXTERNAL_DTD =
- "http://apache.org/xml/features/nonvalidating/load-external-dtd";
- private static final String INDENT_AMOUNT =
- "{http://xml.apache.org/xslt}indent-amount";
- private static final String NAMESPACE = "http://www.w3.org/1999/xhtml";
-
- private static final ByteArrayOutputStream sWriter =
- new ByteArrayOutputStream( 65536 );
- private static final OutputStreamWriter sOutput =
- new OutputStreamWriter( sWriter, UTF_8 );
-
- /**
- * Caches {@link XPathExpression}s to avoid re-compiling.
- */
- private static final Map<String, XPathExpression> sXpaths = new HashMap<>();
-
- private static final DocumentBuilderFactory sDocumentFactory;
- private static DocumentBuilder sDocumentBuilder;
- private static Transformer sTransformer;
- private static final XPath sXpath = XPathFactory.newInstance().newXPath();
-
- public static final DOMImplementation sDomImplementation;
-
- static {
- sDocumentFactory = DocumentBuilderFactory.newInstance();
-
- sDocumentFactory.setValidating( false );
- sDocumentFactory.setAttribute( LOAD_EXTERNAL_DTD, false );
- sDocumentFactory.setNamespaceAware( true );
- sDocumentFactory.setIgnoringComments( true );
- sDocumentFactory.setIgnoringElementContentWhitespace( true );
-
- DOMImplementation domImplementation;
-
- try {
- sDocumentBuilder = sDocumentFactory.newDocumentBuilder();
- domImplementation = sDocumentBuilder.getDOMImplementation();
- sTransformer = TransformerFactory.newInstance().newTransformer();
-
- // Ensure Unicode characters (emojis) are encoded correctly.
- sTransformer.setOutputProperty( ENCODING, UTF_16.toString() );
- sTransformer.setOutputProperty( OMIT_XML_DECLARATION, "yes" );
- sTransformer.setOutputProperty( METHOD, "xml" );
- sTransformer.setOutputProperty( INDENT, "no" );
- sTransformer.setOutputProperty( INDENT_AMOUNT, "2" );
- } catch( final Exception ex ) {
- clue( ex );
- domImplementation = sDocumentBuilder.getDOMImplementation();
- }
-
- sDomImplementation = domImplementation;
- }
-
- public static Document newDocument() {
- return sDocumentBuilder.newDocument();
- }
-
- /**
- * Creates a new document object model based on the given XML document
- * string. This will return an empty document if the document could not
- * be parsed.
- *
- * @param xml The document text to convert into a DOM.
- * @return The DOM that represents the given XML data.
- */
- public static Document parse( final String xml ) {
- assert xml != null;
-
- final var input = new InputSource();
-
- try( final var reader = new StringReader( xml ) ) {
- input.setEncoding( UTF_8.toString() );
- input.setCharacterStream( reader );
-
- return sDocumentBuilder.parse( input );
- } catch( final Throwable t ) {
- clue( t );
-
- return sDocumentBuilder.newDocument();
- }
- }
-
- /**
- * Creates a well-formed XHTML document from a standard HTML document.
- *
- * @param source The HTML source document to transform.
- * @param metadata The metadata contained within the head element.
- * @param locale The localization information for the lang attribute.
- * @return The well-formed XHTML document.
- */
- public static Document create(
- final Document source,
- final Map<String, String> metadata,
- final Locale locale
- ) {
-
- final var root = source.getDocumentElement();
- root.setAttribute( "xmlns", NAMESPACE );
-
- final var doc = newXhtmlDocument( NAMESPACE );
- final var html = doc.getDocumentElement();
- final var head = createElement( doc, "head", null );
-
- html.setAttribute( "lang", locale.getLanguage() );
-
- final var encoding = createEncoding( doc, "UTF-8" );
- head.appendChild( encoding );
-
- for( final var entry : metadata.entrySet() ) {
- final var node = createMeta( doc, entry );
- head.appendChild( node );
- }
-
- final var title = createElement( doc, "title", "Generated Page" );
- head.appendChild( title );
- html.appendChild( head );
-
- final var body = createElement( doc, "body", null );
- final var sourceBody = source.getElementsByTagName( "body" ).item( 0 );
- final var children = sourceBody.getChildNodes();
- final var count = children.getLength();
-
- for( var i = 0; i < count; i++ ) {
- final var imported = importNode( doc, children.item( i ), NAMESPACE );
- body.appendChild( imported );
- }
-
- html.appendChild( body );
-
- return doc;
- }
-
- private static Node importNode(
- final Document targetDoc,
- final Node sourceNode,
- final String namespace ) {
-
- if( sourceNode.getNodeType() == Node.ELEMENT_NODE ) {
- // Create element in target namespace
- final var element = targetDoc.createElementNS(
- namespace,
- sourceNode.getNodeName()
- );
-
- // Copy attributes
- final var attrs = sourceNode.getAttributes();
- if( attrs != null ) {
- for( int i = 0; i < attrs.getLength(); i++ ) {
- final var attr = attrs.item( i );
- element.setAttribute( attr.getNodeName(), attr.getNodeValue() );
- }
- }
-
- // Recursively copy children
- final var children = sourceNode.getChildNodes();
- for( int i = 0; i < children.getLength(); i++ ) {
- final var child = importNode(
- targetDoc,
- children.item( i ), namespace
- );
- element.appendChild( child );
- }
-
- return element;
- }
- else if( sourceNode.getNodeType() == Node.TEXT_NODE ) {
- return targetDoc.createTextNode( sourceNode.getNodeValue() );
- }
- else {
- // For other node types, fall back to regular import
- return targetDoc.importNode( sourceNode, true );
- }
- }
-
- public static Document newXhtmlDocument( final String namespace ) {
- return sDomImplementation.createDocument(
- namespace,
- "html",
- sDomImplementation.createDocumentType(
- "html", "-//W3C//DTD XHTML 1.0 Strict//EN",
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
- )
- );
- }
-
- /**
- * Parses the given file contents into a document object model.
- *
- * @param doc The source XML document to parse.
- * @return The file as a document object model.
- * @throws IOException Could not open the document.
- * @throws SAXException Could not read the XML file content.
- */
- public static Document parse( final File doc )
- throws IOException, SAXException {
- assert doc != null;
-
- try( final var in = new FileInputStream( doc ) ) {
- return parse( in );
- }
- }
-
- /**
- * Parses the given file contents into a document object model. Callers
- * must close the stream.
- *
- * @param doc The source XML document to parse.
- * @return The {@link InputStream} converted to a document object model.
- * @throws IOException Could not open the document.
- * @throws SAXException Could not read the XML file content.
- */
- public static Document parse( final InputStream doc )
- throws IOException, SAXException {
- assert doc != null;
-
- return sDocumentBuilder.parse( doc );
- }
-
- /**
- * Allows an operation to be applied for every node in the document that
- * matches a given tag name pattern.
- *
- * @param document Document to traverse.
- * @param xpath Document elements to find via {@link XPath} expression.
- * @param consumer The consumer to call for each matching document node.
- */
- public static void visit(
- final Document document,
- final CharSequence xpath,
- final Consumer<Node> consumer ) {
- assert document != null;
- assert consumer != null;
-
- try {
- final var expr = compile( xpath );
- final var nodeSet = expr.evaluate( document, NODESET );
-
- if( nodeSet instanceof NodeList nodes ) {
- for( int i = 0, len = nodes.getLength(); i < len; i++ ) {
- consumer.accept( nodes.item( i ) );
- }
- }
- } catch( final Exception ex ) {
- clue( ex );
- }
- }
-
- public static Node createMeta(
- final Document document, final Map.Entry<String, String> entry ) {
- assert document != null;
- assert entry != null;
-
- final var node = document.createElement( "meta" );
-
- node.setAttribute( "name", entry.getKey() );
- node.setAttribute( "content", entry.getValue() );
-
- return node;
- }
-
- public static Node createEncoding(
- final Document document, final String encoding
- ) {
- assert document != null;
- assert encoding != null;
-
- final var node = document.createElement( "meta" );
-
- node.setAttribute( "http-equiv", "Content-Type" );
- node.setAttribute( "content", "text/html; charset=" + encoding );
-
- return node;
- }
-
- public static Element createElement(
- final Document doc, final String nodeName, final String nodeValue ) {
- assert doc != null;
- assert nodeName != null;
- assert !nodeName.isBlank();
-
- final var node = doc.createElement( nodeName );
-
- if( nodeValue != null ) {
- node.setTextContent( nodeValue );
- }
-
- return node;
- }
-
- public static String toString( final Document xhtml ) {
- assert xhtml != null;
-
- try( final var writer = new StringWriter() ) {
- final var result = new StreamResult( writer );
-
- transform( xhtml, result );
-
- return writer.toString();
- } catch( final Exception ex ) {
- clue( ex );
- return "";
- }
- }
-
- public static String transform( final Element root )
- throws IOException, TransformerException {
- assert root != null;
-
- try( final var writer = new StringWriter() ) {
- transform( root.getOwnerDocument(), new StreamResult( writer ) );
-
- return writer.toString();
- }
- }
-
- /**
- * Remove whitespace, comments, and XML/DOCTYPE declarations to make
- * processing work with ConTeXt.
- *
- * @param path The SVG file to process.
- * @throws Exception The file could not be processed.
- */
- public static void sanitize( final Path path ) throws Exception {
- assert path != null;
-
- // Preprocessing the SVG image is a single-threaded operation, no matter
- // how many SVG images are in the document to typeset.
- sWriter.reset();
-
- final var target = new StreamResult( sOutput );
- final var source = sDocumentBuilder.parse( toFile( path ) );
-
- transform( source, target );
- write( path, sWriter.toByteArray() );
- }
-
- /**
- * Converts a string into an {@link XPathExpression}, which may be used to
- * extract elements from a {@link Document} object model.
- *
- * @param cs The string to convert to an {@link XPathExpression}.
- * @return {@code null} if there was an error compiling the xpath.
- */
- public static XPathExpression compile( final CharSequence cs ) {
- assert cs != null;
-
- final var xpath = cs.toString();
-
- return sXpaths.computeIfAbsent(
- xpath, _ -> {
- try {
- return sXpath.compile( xpath );
- } catch( final XPathExpressionException ex ) {
- clue( ex );
- return null;
- }
- }
+import com.keenwrite.util.Strings;
+import org.w3c.dom.*;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import java.io.*;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Consumer;
+
+import static com.keenwrite.events.StatusEvent.clue;
+import static com.keenwrite.io.SysFile.toFile;
+import static java.nio.charset.StandardCharsets.UTF_16;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.nio.file.Files.write;
+import static javax.xml.transform.OutputKeys.*;
+import static javax.xml.xpath.XPathConstants.NODESET;
+
+/**
+ * Responsible for initializing an XML parser.
+ */
+public class DocumentParser {
+ private static final String LOAD_EXTERNAL_DTD =
+ "http://apache.org/xml/features/nonvalidating/load-external-dtd";
+ private static final String INDENT_AMOUNT =
+ "{http://xml.apache.org/xslt}indent-amount";
+ private static final String NAMESPACE = "http://www.w3.org/1999/xhtml";
+
+ private static final ByteArrayOutputStream sWriter =
+ new ByteArrayOutputStream( 65536 );
+ private static final OutputStreamWriter sOutput =
+ new OutputStreamWriter( sWriter, UTF_8 );
+
+ /**
+ * Caches {@link XPathExpression}s to avoid re-compiling.
+ */
+ private static final Map<String, XPathExpression> sXpaths = new HashMap<>();
+
+ private static final DocumentBuilderFactory sDocumentFactory;
+ private static DocumentBuilder sDocumentBuilder;
+ private static Transformer sTransformer;
+ private static final XPath sXpath = XPathFactory.newInstance().newXPath();
+
+ public static final DOMImplementation sDomImplementation;
+
+ static {
+ sDocumentFactory = DocumentBuilderFactory.newInstance();
+
+ sDocumentFactory.setValidating( false );
+ sDocumentFactory.setAttribute( LOAD_EXTERNAL_DTD, false );
+ sDocumentFactory.setNamespaceAware( true );
+ sDocumentFactory.setIgnoringComments( true );
+ sDocumentFactory.setIgnoringElementContentWhitespace( true );
+
+ DOMImplementation domImplementation;
+
+ try {
+ sDocumentBuilder = sDocumentFactory.newDocumentBuilder();
+ domImplementation = sDocumentBuilder.getDOMImplementation();
+ sTransformer = TransformerFactory.newInstance().newTransformer();
+
+ // Ensure Unicode characters (emojis) are encoded correctly.
+ sTransformer.setOutputProperty( ENCODING, UTF_16.toString() );
+ sTransformer.setOutputProperty( OMIT_XML_DECLARATION, "yes" );
+ sTransformer.setOutputProperty( METHOD, "xml" );
+ sTransformer.setOutputProperty( INDENT, "no" );
+ sTransformer.setOutputProperty( INDENT_AMOUNT, "2" );
+ } catch( final Exception ex ) {
+ clue( ex );
+ domImplementation = sDocumentBuilder.getDOMImplementation();
+ }
+
+ sDomImplementation = domImplementation;
+ }
+
+ public static Document newDocument() {
+ return sDocumentBuilder.newDocument();
+ }
+
+ /**
+ * Creates a new document object model based on the given XML document
+ * string. This will return an empty document if the document could not
+ * be parsed.
+ *
+ * @param xml The document text to convert into a DOM.
+ * @return The DOM that represents the given XML data.
+ */
+ public static Document parse( final String xml ) {
+ assert xml != null;
+
+ final var input = new InputSource();
+
+ try( final var reader = new StringReader( xml ) ) {
+ input.setEncoding( UTF_8.toString() );
+ input.setCharacterStream( reader );
+
+ return sDocumentBuilder.parse( input );
+ } catch( final Throwable t ) {
+ clue( t );
+
+ return sDocumentBuilder.newDocument();
+ }
+ }
+
+ /**
+ * Creates a well-formed XHTML document from a standard HTML document.
+ *
+ * @param source The HTML source document to transform.
+ * @param metadata The metadata contained within the head element.
+ * @param locale The localization information for the lang attribute.
+ * @return The well-formed XHTML document.
+ */
+ public static Document create(
+ final Document source,
+ final Map<String, String> metadata,
+ final Locale locale,
+ final String pageTitle
+ ) {
+ final var root = source.getDocumentElement();
+ root.setAttribute( "xmlns", NAMESPACE );
+
+ final var doc = createXhtmlDocument();
+ final var html = doc.getDocumentElement();
+ final var head = createElement( doc, "head", null );
+
+ html.setAttribute( "lang", locale.getLanguage() );
+
+ final var encoding = createEncoding( doc, "UTF-8" );
+ head.appendChild( encoding );
+
+ for( final var entry : metadata.entrySet() ) {
+ final var node = createMeta( doc, entry );
+ head.appendChild( node );
+ }
+
+ final var titleText = Strings.sanitize( pageTitle );
+
+ // Empty titles result in <title/>, which some browsers do not parse.
+ if( !titleText.isEmpty() ) {
+ final var title = createElement( doc, "title", titleText );
+ head.appendChild( title );
+ }
+
+ html.appendChild( head );
+
+ final var body = createElement( doc, "body", null );
+ final var sourceBody = source.getElementsByTagName( "body" ).item( 0 );
+ final var children = sourceBody.getChildNodes();
+ final var count = children.getLength();
+
+ for( var i = 0; i < count; i++ ) {
+ body.appendChild( importNode( doc, children.item( i ) ) );
+ }
+
+ html.appendChild( body );
+
+ return doc;
+ }
+
+ /**
+ * Parses the given file contents into a document object model.
+ *
+ * @param doc The source XML document to parse.
+ * @return The file as a document object model.
+ * @throws IOException Could not open the document.
+ * @throws SAXException Could not read the XML file content.
+ */
+ public static Document parse( final File doc )
+ throws IOException, SAXException {
+ assert doc != null;
+
+ try( final var in = new FileInputStream( doc ) ) {
+ return parse( in );
+ }
+ }
+
+ /**
+ * Parses the given file contents into a document object model. Callers
+ * must close the stream.
+ *
+ * @param doc The source XML document to parse.
+ * @return The {@link InputStream} converted to a document object model.
+ * @throws IOException Could not open the document.
+ * @throws SAXException Could not read the XML file content.
+ */
+ public static Document parse( final InputStream doc )
+ throws IOException, SAXException {
+ assert doc != null;
+
+ return sDocumentBuilder.parse( doc );
+ }
+
+ /**
+ * Allows an operation to be applied for every node in the document that
+ * matches a given tag name pattern.
+ *
+ * @param document Document to traverse.
+ * @param xpath Document elements to find via {@link XPath} expression.
+ * @param consumer The consumer to call for each matching document node.
+ */
+ public static void visit(
+ final Document document,
+ final CharSequence xpath,
+ final Consumer<Node> consumer ) {
+ assert document != null;
+ assert consumer != null;
+
+ try {
+ final var expr = compile( xpath );
+ final var nodeSet = expr.evaluate( document, NODESET );
+
+ if( nodeSet instanceof NodeList nodes ) {
+ for( int i = 0, len = nodes.getLength(); i < len; i++ ) {
+ consumer.accept( nodes.item( i ) );
+ }
+ }
+ } catch( final Exception ex ) {
+ clue( ex );
+ }
+ }
+
+ public static Node createMeta(
+ final Document document, final Map.Entry<String, String> entry ) {
+ assert document != null;
+ assert entry != null;
+
+ final var node = document.createElement( "meta" );
+
+ node.setAttribute( "name", entry.getKey() );
+ node.setAttribute( "content", entry.getValue() );
+
+ return node;
+ }
+
+ public static Node createEncoding(
+ final Document document, final String encoding
+ ) {
+ assert document != null;
+ assert encoding != null;
+
+ final var node = document.createElement( "meta" );
+
+ node.setAttribute( "http-equiv", "Content-Type" );
+ node.setAttribute( "content", "text/html; charset=" + encoding );
+
+ return node;
+ }
+
+ public static Element createElement(
+ final Document doc, final String nodeName, final String nodeValue ) {
+ assert doc != null;
+ assert nodeName != null;
+ assert !nodeName.isBlank();
+
+ final var node = doc.createElement( nodeName );
+
+ if( nodeValue != null ) {
+ node.setTextContent( nodeValue );
+ }
+
+ return node;
+ }
+
+ public static String toString( final Document xhtml ) {
+ assert xhtml != null;
+
+ try( final var writer = new StringWriter() ) {
+ final var result = new StreamResult( writer );
+
+ transform( xhtml, result );
+
+ return writer.toString();
+ } catch( final Exception ex ) {
+ clue( ex );
+ return "";
+ }
+ }
+
+ public static String transform( final Element root )
+ throws IOException, TransformerException {
+ assert root != null;
+
+ try( final var writer = new StringWriter() ) {
+ transform( root.getOwnerDocument(), new StreamResult( writer ) );
+
+ return writer.toString();
+ }
+ }
+
+ /**
+ * Remove whitespace, comments, and XML/DOCTYPE declarations to make
+ * processing work with ConTeXt.
+ *
+ * @param path The SVG file to process.
+ * @throws Exception The file could not be processed.
+ */
+ public static void sanitize( final Path path ) throws Exception {
+ assert path != null;
+
+ // Preprocessing the SVG image is a single-threaded operation, no matter
+ // how many SVG images are in the document to typeset.
+ sWriter.reset();
+
+ final var target = new StreamResult( sOutput );
+ final var source = sDocumentBuilder.parse( toFile( path ) );
+
+ transform( source, target );
+ write( path, sWriter.toByteArray() );
+ }
+
+ /**
+ * Converts a string into an {@link XPathExpression}, which may be used to
+ * extract elements from a {@link Document} object model.
+ *
+ * @param cs The string to convert to an {@link XPathExpression}.
+ * @return {@code null} if there was an error compiling the xpath.
+ */
+ public static XPathExpression compile( final CharSequence cs ) {
+ assert cs != null;
+
+ final var xpath = cs.toString();
+
+ return sXpaths.computeIfAbsent(
+ xpath, _ -> {
+ try {
+ return sXpath.compile( xpath );
+ } catch( final XPathExpressionException ex ) {
+ clue( ex );
+ return null;
+ }
+ }
+ );
+ }
+
+ /**
+ * Merges a source document into a target document. This avoids adding an
+ * empty XML namespace attribute to elements.
+ *
+ * @param target The document to envelop the source document.
+ * @param source The source document to embed.
+ * @return The target document with the source document included.
+ */
+ private static Node importNode( final Document target, final Node source ) {
+ assert target != null;
+ assert source != null;
+
+ Node result;
+ final var nodeType = source.getNodeType();
+
+ if( nodeType == Node.ELEMENT_NODE ) {
+ final var element = target.createElementNS(
+ NAMESPACE,
+ source.getNodeName()
+ );
+
+ final var attrs = source.getAttributes();
+
+ if( attrs != null ) {
+ final var attrLength = attrs.getLength();
+
+ for( var i = 0; i < attrLength; i++ ) {
+ final var attr = attrs.item( i );
+ element.setAttribute( attr.getNodeName(), attr.getNodeValue() );
+ }
+ }
+
+ final var children = source.getChildNodes();
+ final var childLength = children.getLength();
+
+ for( var i = 0; i < childLength; i++ ) {
+ final var child = importNode(
+ target,
+ children.item( i )
+ );
+ element.appendChild( child );
+ }
+
+ result = element;
+ }
+ else if( nodeType == Node.TEXT_NODE ) {
+ result = target.createTextNode( source.getNodeValue() );
+ }
+ else {
+ result = target.importNode( source, true );
+ }
+
+ return result;
+ }
+
+ private static Document createXhtmlDocument() {
+ return sDomImplementation.createDocument(
+ NAMESPACE,
+ "html",
+ sDomImplementation.createDocumentType(
+ "html", "-//W3C//DTD XHTML 1.0 Strict//EN",
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
+ )
);
}
src/main/java/com/keenwrite/events/StatusEvent.java
import static com.keenwrite.constants.Constants.NEWLINE;
import static com.keenwrite.constants.Constants.STATUS_BAR_OK;
+import static com.keenwrite.util.Strings.sanitize;
import static java.lang.String.format;
import static java.lang.String.join;
*/
public StatusEvent( final String message, final Throwable problem ) {
- mMessage = message == null ? "" : message;
+ mMessage = sanitize( message );
mProblem = problem;
}
final var message = mMessage == null ? "UNKNOWN" : mMessage;
- return format( "%s%s%s",
- message,
- message.isBlank() ? "" : " ",
- mProblem == null ? "" : toEnglish( mProblem ) );
+ return format(
+ "%s%s%s",
+ message,
+ message.isBlank() ? "" : " ",
+ mProblem == null ? "" : toEnglish( mProblem )
+ );
}
src/main/java/com/keenwrite/preferences/AposProperty.java
+package com.keenwrite.preferences;
+
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.ObservableList;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import static com.keenwrite.constants.Constants.APOS_DEFAULT;
+import static com.keenwrite.preferences.Workspace.listProperty;
+
+/**
+ * Maintains a list of apostrophe encodings the user may select.
+ */
+public final class AposProperty extends SimpleObjectProperty<String> {
+ /**
+ * Ordered set of available apostrophe encodings.
+ */
+ private static final Set<String> sProperties = new LinkedHashSet<>();
+
+ static {
+ sProperties.add( "regular" );
+ sProperties.add( "modifier" );
+ sProperties.add( APOS_DEFAULT );
+ sProperties.add( "aposhex" );
+ sProperties.add( "quote" );
+ sProperties.add( "quotehex" );
+ }
+
+ public AposProperty( final String property ) {
+ super( property );
+ }
+
+ /**
+ * Returns the list of available apostrophe types to use when encoding.
+ *
+ * @return A selection of apostrophes.
+ */
+ public static ObservableList<String> aposListProperty() {
+ assert !sProperties.isEmpty();
+
+ return listProperty( sProperties );
+ }
+
+ /**
+ * Ensures that the given property name is in the property list.
+ *
+ * @param property Property to validate.
+ * @return The given property was found, otherwise the default property.
+ */
+ private static String sanitize( final String property ) {
+ assert property != null;
+
+ return sProperties.contains( property ) ? property : APOS_DEFAULT;
+ }
+}
src/main/java/com/keenwrite/preferences/PreferencesController.java
import static com.keenwrite.constants.Constants.USER_DIRECTORY;
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG;
-import static com.keenwrite.preferences.AppKeys.*;
-import static com.keenwrite.preferences.LocaleProperty.localeListProperty;
-import static com.keenwrite.preferences.SkinProperty.skinListProperty;
-import static com.keenwrite.preferences.TableField.ofListType;
-import static javafx.scene.control.ButtonType.CANCEL;
-import static javafx.scene.control.ButtonType.OK;
-
-/**
- * Provides the ability for users to configure their preferences. This links
- * the {@link Workspace} model with the {@link PreferencesFx} view, in MVC.
- */
-@SuppressWarnings( "SameParameterValue" )
-public final class PreferencesController {
-
- private final Workspace mWorkspace;
- private final PreferencesFx mPreferencesFx;
-
- public PreferencesController( final Workspace workspace ) {
- mWorkspace = workspace;
-
- // Order matters: set the workspace before creating the dialog.
- mPreferencesFx = createPreferencesFx();
-
- initKeyEventHandler( mPreferencesFx );
- initSaveEventHandler( mPreferencesFx );
- }
-
- /**
- * Display the user preferences settings dialog (non-modal).
- */
- public void show() {
- mPreferencesFx.show( false );
- }
-
- private StringField createFontNameField(
- final StringProperty fontName, final DoubleProperty fontSize ) {
- final var control = new SimpleFontControl( "Change" );
-
- control.fontSizeProperty().addListener( ( _, _, n ) -> {
- if( n != null ) {
- fontSize.set( n.doubleValue() );
- }
- } );
-
- return ofStringType( fontName ).render( control );
- }
-
- /**
- * Convenience method to create a helper class for the user interface. This
- * establishes a key-value pair for the view.
- *
- * @param persist A reference to the values that will be persisted.
- * @param <K> The type of key, usually a string.
- * @param <V> The type of value, usually a string.
- * @return UI data model container that may update the persistent state.
- */
- private <K, V> TableField<Entry<K, V>> createTableField(
- final ListProperty<Entry<K, V>> persist ) {
- return ofListType( persist ).render( new SimpleTableControl<>() );
- }
-
- /**
- * Creates the preferences dialog using {@link SkeletonStorageHandler} and
- * numerous {@link Category} objects.
- *
- * @return A component for editing preferences.
- * @throws RuntimeException Could not construct the {@link PreferencesFx}
- * object (e.g., illegal access permissions,
- * unmapped XML resource).
- */
- private PreferencesFx createPreferencesFx() {
- return PreferencesFx.of( createStorageHandler(), createCategories() )
- .instantPersistent( false )
- .dialogIcon( ICON_DIALOG );
- }
-
- /**
- * Override the {@link PreferencesFx} storage handler to perform no actions.
- * Persistence is accomplished using the {@link XmlStore}.
- *
- * @return A no-op {@link StorageHandler} implementation.
- */
- private StorageHandler createStorageHandler() {
- return new SkeletonStorageHandler();
- }
-
- private Category[] createCategories() {
- return new Category[]{
- Category.of(
- get( KEY_DOC ),
- Group.of(
- get( KEY_DOC_META ),
- Setting.of( label( KEY_DOC_META ) ),
- Setting.of( title( KEY_DOC_META ),
- createTableField( listEntryProperty( KEY_DOC_META ) ),
- listEntryProperty( KEY_DOC_META ) )
- )
- ),
- Category.of(
- get( KEY_TYPESET ),
- Group.of(
- get( KEY_TYPESET_CONTEXT ),
- Setting.of( label( KEY_TYPESET_CONTEXT_THEMES_PATH ) ),
- Setting.of( title( KEY_TYPESET_CONTEXT_THEMES_PATH ),
- directoryProperty( KEY_TYPESET_CONTEXT_THEMES_PATH ),
- true ),
- Setting.of( label( KEY_TYPESET_CONTEXT_CLEAN ) ),
- Setting.of( title( KEY_TYPESET_CONTEXT_CLEAN ),
- booleanProperty( KEY_TYPESET_CONTEXT_CLEAN ) )
- ),
- Group.of(
- get( KEY_TYPESET_CONTEXT_FONTS ),
- Setting.of( label( KEY_TYPESET_CONTEXT_FONTS_DIR ) ),
- Setting.of( title( KEY_TYPESET_CONTEXT_FONTS_DIR ),
- directoryProperty( KEY_TYPESET_CONTEXT_FONTS_DIR ),
- true )
- ),
- Group.of(
- get( KEY_TYPESET_TYPOGRAPHY ),
- Setting.of( label( KEY_TYPESET_TYPOGRAPHY_QUOTES ) ),
- Setting.of( title( KEY_TYPESET_TYPOGRAPHY_QUOTES ),
- booleanProperty( KEY_TYPESET_TYPOGRAPHY_QUOTES ) )
- ),
- Group.of(
- get( KEY_TYPESET_MODES ),
- Setting.of( label( KEY_TYPESET_MODES_ENABLED ) ),
- Setting.of( title( KEY_TYPESET_MODES_ENABLED ),
- stringProperty( KEY_TYPESET_MODES_ENABLED ) )
- )
- ),
- Category.of(
- get( KEY_EDITOR ),
- Group.of(
- get( KEY_EDITOR_AUTOSAVE ),
- Setting.of( label( KEY_EDITOR_AUTOSAVE ) ),
- Setting.of( title( KEY_EDITOR_AUTOSAVE ),
- integerProperty( KEY_EDITOR_AUTOSAVE ) )
- )
- ),
- Category.of(
- get( KEY_R ),
- Group.of(
- get( KEY_R_DIR ),
- Setting.of( label( KEY_R_DIR ) ),
- Setting.of( title( KEY_R_DIR ),
- directoryProperty( KEY_R_DIR ),
- true )
- ),
- Group.of(
- get( KEY_R_SCRIPT ),
- Setting.of( label( KEY_R_SCRIPT ) ),
- createMultilineSetting( "Script", KEY_R_SCRIPT )
- ),
- Group.of(
- get( KEY_R_DELIM_BEGAN ),
- Setting.of( label( KEY_R_DELIM_BEGAN ) ),
- Setting.of( title( KEY_R_DELIM_BEGAN ),
- stringProperty( KEY_R_DELIM_BEGAN ) )
- ),
- Group.of(
- get( KEY_R_DELIM_ENDED ),
- Setting.of( label( KEY_R_DELIM_ENDED ) ),
- Setting.of( title( KEY_R_DELIM_ENDED ),
- stringProperty( KEY_R_DELIM_ENDED ) )
- )
- ),
- Category.of(
- get( KEY_IMAGE ),
- Group.of(
- get( KEY_IMAGE_DIR ),
- Setting.of( label( KEY_IMAGE_DIR ) ),
- Setting.of( title( KEY_IMAGE_DIR ),
- directoryProperty( KEY_IMAGE_DIR ),
- true ),
- Setting.of( label( KEY_CACHE_DIR ) ),
- Setting.of( title( KEY_CACHE_DIR ),
- directoryProperty( KEY_CACHE_DIR ),
- true )
- ),
- Group.of(
- get( KEY_IMAGE_ORDER ),
- Setting.of( label( KEY_IMAGE_ORDER ) ),
- Setting.of( title( KEY_IMAGE_ORDER ),
- stringProperty( KEY_IMAGE_ORDER ) )
- ),
- Group.of(
- get( KEY_IMAGE_RESIZE ),
- Setting.of( label( KEY_IMAGE_RESIZE ) ),
- Setting.of( title( KEY_IMAGE_RESIZE ),
- booleanProperty( KEY_IMAGE_RESIZE ) )
- ),
- Group.of(
- get( KEY_IMAGE_SERVER ),
- Setting.of( label( KEY_IMAGE_SERVER ) ),
- Setting.of( title( KEY_IMAGE_SERVER ),
- stringProperty( KEY_IMAGE_SERVER ) )
- )
- ),
- Category.of(
- get( KEY_DEF ),
- Group.of(
- get( KEY_DEF_PATH ),
- Setting.of( label( KEY_DEF_PATH ) ),
- Setting.of( title( KEY_DEF_PATH ),
- fileProperty( KEY_DEF_PATH ), false )
- ),
- Group.of(
- get( KEY_DEF_DELIM_BEGAN ),
- Setting.of( label( KEY_DEF_DELIM_BEGAN ) ),
- Setting.of( title( KEY_DEF_DELIM_BEGAN ),
- stringProperty( KEY_DEF_DELIM_BEGAN ) )
- ),
- Group.of(
- get( KEY_DEF_DELIM_ENDED ),
- Setting.of( label( KEY_DEF_DELIM_ENDED ) ),
- Setting.of( title( KEY_DEF_DELIM_ENDED ),
- stringProperty( KEY_DEF_DELIM_ENDED ) )
- )
- ),
- Category.of(
- get( KEY_UI_FONT ),
- Group.of(
- get( KEY_UI_FONT_EDITOR ),
- Setting.of( label( KEY_UI_FONT_EDITOR_NAME ) ),
- Setting.of( title( KEY_UI_FONT_EDITOR_NAME ),
- createFontNameField(
- stringProperty( KEY_UI_FONT_EDITOR_NAME ),
- doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) ),
- stringProperty( KEY_UI_FONT_EDITOR_NAME ) ),
- Setting.of( label( KEY_UI_FONT_EDITOR_SIZE ) ),
- Setting.of( title( KEY_UI_FONT_EDITOR_SIZE ),
- doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) )
- ),
- Group.of(
- get( KEY_UI_FONT_PREVIEW ),
- Setting.of( label( KEY_UI_FONT_PREVIEW_NAME ) ),
- Setting.of( title( KEY_UI_FONT_PREVIEW_NAME ),
- createFontNameField(
- stringProperty( KEY_UI_FONT_PREVIEW_NAME ),
- doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) ),
- stringProperty( KEY_UI_FONT_PREVIEW_NAME ) ),
- Setting.of( label( KEY_UI_FONT_PREVIEW_SIZE ) ),
- Setting.of( title( KEY_UI_FONT_PREVIEW_SIZE ),
- doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) ),
- Setting.of( label( KEY_UI_FONT_PREVIEW_MONO_NAME ) ),
- Setting.of( title( KEY_UI_FONT_PREVIEW_MONO_NAME ),
- createFontNameField(
- stringProperty( KEY_UI_FONT_PREVIEW_MONO_NAME ),
- doubleProperty( KEY_UI_FONT_PREVIEW_MONO_SIZE ) ),
- stringProperty( KEY_UI_FONT_PREVIEW_MONO_NAME ) ),
- Setting.of( label( KEY_UI_FONT_PREVIEW_MONO_SIZE ) ),
- Setting.of( title( KEY_UI_FONT_PREVIEW_MONO_SIZE ),
- doubleProperty( KEY_UI_FONT_PREVIEW_MONO_SIZE ) )
- ),
- Group.of(
- get( KEY_UI_FONT_MATH ),
- Setting.of( title( KEY_UI_FONT_MATH_SIZE ),
- doubleProperty( KEY_UI_FONT_MATH_SIZE ) )
- )
- ),
- Category.of(
- get( KEY_UI_SKIN ),
- Group.of(
- get( KEY_UI_SKIN_SELECTION ),
- Setting.of( label( KEY_UI_SKIN_SELECTION ) ),
- Setting.of( title( KEY_UI_SKIN_SELECTION ),
- skinListProperty(),
- skinProperty( KEY_UI_SKIN_SELECTION ) )
- ),
- Group.of(
- get( KEY_UI_SKIN_CUSTOM ),
- Setting.of( label( KEY_UI_SKIN_CUSTOM ) ),
- Setting.of( title( KEY_UI_SKIN_CUSTOM ),
- fileProperty( KEY_UI_SKIN_CUSTOM ), false )
- )
- ),
- Category.of(
- get( KEY_UI_PREVIEW ),
- Group.of(
- get( KEY_UI_PREVIEW_STYLESHEET ),
- Setting.of( label( KEY_UI_PREVIEW_STYLESHEET ) ),
- Setting.of( title( KEY_UI_PREVIEW_STYLESHEET ),
- fileProperty( KEY_UI_PREVIEW_STYLESHEET ), false )
- )
- ),
- Category.of(
- get( KEY_LANGUAGE ),
- Group.of(
- get( KEY_LANGUAGE_LOCALE ),
- Setting.of( label( KEY_LANGUAGE_LOCALE ) ),
- Setting.of( title( KEY_LANGUAGE_LOCALE ),
- localeListProperty(),
- localeProperty( KEY_LANGUAGE_LOCALE ) )
- )
- )
- };
- }
-
- @SuppressWarnings( "unchecked" )
- private Setting<StringField, StringProperty> createMultilineSetting(
- final String description, final Key property ) {
- final Setting<StringField, StringProperty> setting =
- Setting.of( description, stringProperty( property ) );
- final var field = setting.getElement();
- field.multiline( true );
-
- return setting;
- }
-
- /**
- * Map ENTER and ESCAPE keys to OK and CANCEL buttons, respectively.
- */
- private void initKeyEventHandler( final PreferencesFx preferences ) {
- final var view = preferences.getView();
- final var nodes = view.getChildrenUnmodifiable();
- final var master = (MasterDetailPane) nodes.getFirst();
- final var detail = (NavigationView) master.getDetailNode();
- final var pane = (DialogPane) view.getParent();
-
- detail.setOnKeyReleased( key -> {
- switch( key.getCode() ) {
- case ENTER -> ((Button) pane.lookupButton( OK )).fire();
- case ESCAPE -> ((Button) pane.lookupButton( CANCEL )).fire();
- default -> {}
- }
- } );
- }
-
- /**
- * Called when the user clicks the APPLY or OK buttons in the dialog.
- *
- * @param preferences Preferences widget.
- */
- private void initSaveEventHandler( final PreferencesFx preferences ) {
- preferences.addEventHandler(
- EVENT_PREFERENCES_SAVED, _ -> mWorkspace.save()
- );
- }
-
- /**
- * Creates a label for the given key after interpolating its value.
- *
- * @param key The key to find in the resource bundle.
- * @return The value of the key as a label.
- */
- private Node label( final Key key ) {
- return label( key, (String[]) null );
- }
-
- private Node label( final Key key, final String... values ) {
- return new Label( get( String.format( "%s%s", key.toString(), ".desc" ), (Object[]) values ) );
- }
-
- private String title( final Key key ) {
- return get( String.format( "%s%s", key.toString(), ".title" ) );
- }
-
- /**
- * Screens out non-existent directories to avoid throwing an exception caused
- * by
- * <a href="https://github.com/dlsc-software-consulting-gmbh/PreferencesFX/issues/441">
- * PreferencesFX issue #441
- * </a>.
- *
- * @param key Preference to pre-screen before creating a {@link FileProperty}.
- * @return The preferred value or the user's home directory if the directory
- * does not exist.
- */
- private ObjectProperty<File> directoryProperty( final Key key ) {
- final var property = mWorkspace.fileProperty( key );
- final var file = property.get();
-
- if( !file.exists() ) {
- property.set( USER_DIRECTORY );
- }
-
- return property;
- }
-
- private ObjectProperty<File> fileProperty( final Key key ) {
- return mWorkspace.fileProperty( key );
- }
-
- private StringProperty stringProperty( final Key key ) {
- return mWorkspace.stringProperty( key );
- }
-
- private BooleanProperty booleanProperty( final Key key ) {
- return mWorkspace.booleanProperty( key );
- }
-
- private IntegerProperty integerProperty( final Key key ) {
- return mWorkspace.integerProperty( key );
- }
-
- private DoubleProperty doubleProperty( final Key key ) {
- return mWorkspace.doubleProperty( key );
- }
-
- private ObjectProperty<String> skinProperty( final Key key ) {
- return mWorkspace.skinProperty( key );
+import static com.keenwrite.preferences.AposProperty.aposListProperty;
+import static com.keenwrite.preferences.AppKeys.*;
+import static com.keenwrite.preferences.LocaleProperty.localeListProperty;
+import static com.keenwrite.preferences.SkinProperty.skinListProperty;
+import static com.keenwrite.preferences.TableField.ofListType;
+import static javafx.scene.control.ButtonType.CANCEL;
+import static javafx.scene.control.ButtonType.OK;
+
+/**
+ * Provides the ability for users to configure their preferences. This links
+ * the {@link Workspace} model with the {@link PreferencesFx} view, in MVC.
+ */
+@SuppressWarnings( "SameParameterValue" )
+public final class PreferencesController {
+
+ private final Workspace mWorkspace;
+ private final PreferencesFx mPreferencesFx;
+
+ public PreferencesController( final Workspace workspace ) {
+ mWorkspace = workspace;
+
+ // Order matters: set the workspace before creating the dialog.
+ mPreferencesFx = createPreferencesFx();
+
+ initKeyEventHandler( mPreferencesFx );
+ initSaveEventHandler( mPreferencesFx );
+ }
+
+ /**
+ * Display the user preferences settings dialog (non-modal).
+ */
+ public void show() {
+ mPreferencesFx.show( false );
+ }
+
+ private StringField createFontNameField(
+ final StringProperty fontName, final DoubleProperty fontSize ) {
+ final var control = new SimpleFontControl( "Change" );
+
+ control.fontSizeProperty().addListener( ( _, _, n ) -> {
+ if( n != null ) {
+ fontSize.set( n.doubleValue() );
+ }
+ } );
+
+ return ofStringType( fontName ).render( control );
+ }
+
+ /**
+ * Convenience method to create a helper class for the user interface. This
+ * establishes a key-value pair for the view.
+ *
+ * @param persist A reference to the values that will be persisted.
+ * @param <K> The type of key, usually a string.
+ * @param <V> The type of value, usually a string.
+ * @return UI data model container that may update the persistent state.
+ */
+ private <K, V> TableField<Entry<K, V>> createTableField(
+ final ListProperty<Entry<K, V>> persist ) {
+ return ofListType( persist ).render( new SimpleTableControl<>() );
+ }
+
+ /**
+ * Creates the preferences dialog using {@link SkeletonStorageHandler} and
+ * numerous {@link Category} objects.
+ *
+ * @return A component for editing preferences.
+ * @throws RuntimeException Could not construct the {@link PreferencesFx}
+ * object (e.g., illegal access permissions,
+ * unmapped XML resource).
+ */
+ private PreferencesFx createPreferencesFx() {
+ return PreferencesFx.of( createStorageHandler(), createCategories() )
+ .instantPersistent( false )
+ .dialogIcon( ICON_DIALOG );
+ }
+
+ /**
+ * Override the {@link PreferencesFx} storage handler to perform no actions.
+ * Persistence is accomplished using the {@link XmlStore}.
+ *
+ * @return A no-op {@link StorageHandler} implementation.
+ */
+ private StorageHandler createStorageHandler() {
+ return new SkeletonStorageHandler();
+ }
+
+ private Category[] createCategories() {
+ return new Category[]{
+ Category.of(
+ get( KEY_DOC ),
+ Group.of(
+ get( KEY_DOC_META ),
+ Setting.of( label( KEY_DOC_META ) ),
+ Setting.of( title( KEY_DOC_META ),
+ createTableField( listEntryProperty( KEY_DOC_META ) ),
+ listEntryProperty( KEY_DOC_META ) )
+ )
+ ),
+ Category.of(
+ get( KEY_TYPESET ),
+ Group.of(
+ get( KEY_TYPESET_CONTEXT ),
+ Setting.of( label( KEY_TYPESET_CONTEXT_THEMES_PATH ) ),
+ Setting.of( title( KEY_TYPESET_CONTEXT_THEMES_PATH ),
+ directoryProperty( KEY_TYPESET_CONTEXT_THEMES_PATH ),
+ true ),
+ Setting.of( label( KEY_TYPESET_CONTEXT_CLEAN ) ),
+ Setting.of( title( KEY_TYPESET_CONTEXT_CLEAN ),
+ booleanProperty( KEY_TYPESET_CONTEXT_CLEAN ) )
+ ),
+ Group.of(
+ get( KEY_TYPESET_CONTEXT_FONTS ),
+ Setting.of( label( KEY_TYPESET_CONTEXT_FONTS_DIR ) ),
+ Setting.of( title( KEY_TYPESET_CONTEXT_FONTS_DIR ),
+ directoryProperty( KEY_TYPESET_CONTEXT_FONTS_DIR ),
+ true )
+ ),
+ Group.of(
+ get( KEY_TYPESET_TYPOGRAPHY ),
+ Setting.of( label( KEY_TYPESET_TYPOGRAPHY_QUOTES ) ),
+ Setting.of( title( KEY_TYPESET_TYPOGRAPHY_QUOTES ),
+ aposListProperty(),
+ listProperty( KEY_TYPESET_TYPOGRAPHY_QUOTES ) )
+ ),
+ Group.of(
+ get( KEY_TYPESET_MODES ),
+ Setting.of( label( KEY_TYPESET_MODES_ENABLED ) ),
+ Setting.of( title( KEY_TYPESET_MODES_ENABLED ),
+ stringProperty( KEY_TYPESET_MODES_ENABLED ) )
+ )
+ ),
+ Category.of(
+ get( KEY_EDITOR ),
+ Group.of(
+ get( KEY_EDITOR_AUTOSAVE ),
+ Setting.of( label( KEY_EDITOR_AUTOSAVE ) ),
+ Setting.of( title( KEY_EDITOR_AUTOSAVE ),
+ integerProperty( KEY_EDITOR_AUTOSAVE ) )
+ )
+ ),
+ Category.of(
+ get( KEY_R ),
+ Group.of(
+ get( KEY_R_DIR ),
+ Setting.of( label( KEY_R_DIR ) ),
+ Setting.of( title( KEY_R_DIR ),
+ directoryProperty( KEY_R_DIR ),
+ true )
+ ),
+ Group.of(
+ get( KEY_R_SCRIPT ),
+ Setting.of( label( KEY_R_SCRIPT ) ),
+ createMultilineSetting( "Script", KEY_R_SCRIPT )
+ ),
+ Group.of(
+ get( KEY_R_DELIM_BEGAN ),
+ Setting.of( label( KEY_R_DELIM_BEGAN ) ),
+ Setting.of( title( KEY_R_DELIM_BEGAN ),
+ stringProperty( KEY_R_DELIM_BEGAN ) )
+ ),
+ Group.of(
+ get( KEY_R_DELIM_ENDED ),
+ Setting.of( label( KEY_R_DELIM_ENDED ) ),
+ Setting.of( title( KEY_R_DELIM_ENDED ),
+ stringProperty( KEY_R_DELIM_ENDED ) )
+ )
+ ),
+ Category.of(
+ get( KEY_IMAGE ),
+ Group.of(
+ get( KEY_IMAGE_DIR ),
+ Setting.of( label( KEY_IMAGE_DIR ) ),
+ Setting.of( title( KEY_IMAGE_DIR ),
+ directoryProperty( KEY_IMAGE_DIR ),
+ true ),
+ Setting.of( label( KEY_CACHE_DIR ) ),
+ Setting.of( title( KEY_CACHE_DIR ),
+ directoryProperty( KEY_CACHE_DIR ),
+ true )
+ ),
+ Group.of(
+ get( KEY_IMAGE_ORDER ),
+ Setting.of( label( KEY_IMAGE_ORDER ) ),
+ Setting.of( title( KEY_IMAGE_ORDER ),
+ stringProperty( KEY_IMAGE_ORDER ) )
+ ),
+ Group.of(
+ get( KEY_IMAGE_RESIZE ),
+ Setting.of( label( KEY_IMAGE_RESIZE ) ),
+ Setting.of( title( KEY_IMAGE_RESIZE ),
+ booleanProperty( KEY_IMAGE_RESIZE ) )
+ ),
+ Group.of(
+ get( KEY_IMAGE_SERVER ),
+ Setting.of( label( KEY_IMAGE_SERVER ) ),
+ Setting.of( title( KEY_IMAGE_SERVER ),
+ stringProperty( KEY_IMAGE_SERVER ) )
+ )
+ ),
+ Category.of(
+ get( KEY_DEF ),
+ Group.of(
+ get( KEY_DEF_PATH ),
+ Setting.of( label( KEY_DEF_PATH ) ),
+ Setting.of( title( KEY_DEF_PATH ),
+ fileProperty( KEY_DEF_PATH ), false )
+ ),
+ Group.of(
+ get( KEY_DEF_DELIM_BEGAN ),
+ Setting.of( label( KEY_DEF_DELIM_BEGAN ) ),
+ Setting.of( title( KEY_DEF_DELIM_BEGAN ),
+ stringProperty( KEY_DEF_DELIM_BEGAN ) )
+ ),
+ Group.of(
+ get( KEY_DEF_DELIM_ENDED ),
+ Setting.of( label( KEY_DEF_DELIM_ENDED ) ),
+ Setting.of( title( KEY_DEF_DELIM_ENDED ),
+ stringProperty( KEY_DEF_DELIM_ENDED ) )
+ )
+ ),
+ Category.of(
+ get( KEY_UI_FONT ),
+ Group.of(
+ get( KEY_UI_FONT_EDITOR ),
+ Setting.of( label( KEY_UI_FONT_EDITOR_NAME ) ),
+ Setting.of( title( KEY_UI_FONT_EDITOR_NAME ),
+ createFontNameField(
+ stringProperty( KEY_UI_FONT_EDITOR_NAME ),
+ doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) ),
+ stringProperty( KEY_UI_FONT_EDITOR_NAME ) ),
+ Setting.of( label( KEY_UI_FONT_EDITOR_SIZE ) ),
+ Setting.of( title( KEY_UI_FONT_EDITOR_SIZE ),
+ doubleProperty( KEY_UI_FONT_EDITOR_SIZE ) )
+ ),
+ Group.of(
+ get( KEY_UI_FONT_PREVIEW ),
+ Setting.of( label( KEY_UI_FONT_PREVIEW_NAME ) ),
+ Setting.of( title( KEY_UI_FONT_PREVIEW_NAME ),
+ createFontNameField(
+ stringProperty( KEY_UI_FONT_PREVIEW_NAME ),
+ doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) ),
+ stringProperty( KEY_UI_FONT_PREVIEW_NAME ) ),
+ Setting.of( label( KEY_UI_FONT_PREVIEW_SIZE ) ),
+ Setting.of( title( KEY_UI_FONT_PREVIEW_SIZE ),
+ doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ) ),
+ Setting.of( label( KEY_UI_FONT_PREVIEW_MONO_NAME ) ),
+ Setting.of( title( KEY_UI_FONT_PREVIEW_MONO_NAME ),
+ createFontNameField(
+ stringProperty( KEY_UI_FONT_PREVIEW_MONO_NAME ),
+ doubleProperty( KEY_UI_FONT_PREVIEW_MONO_SIZE ) ),
+ stringProperty( KEY_UI_FONT_PREVIEW_MONO_NAME ) ),
+ Setting.of( label( KEY_UI_FONT_PREVIEW_MONO_SIZE ) ),
+ Setting.of( title( KEY_UI_FONT_PREVIEW_MONO_SIZE ),
+ doubleProperty( KEY_UI_FONT_PREVIEW_MONO_SIZE ) )
+ ),
+ Group.of(
+ get( KEY_UI_FONT_MATH ),
+ Setting.of( title( KEY_UI_FONT_MATH_SIZE ),
+ doubleProperty( KEY_UI_FONT_MATH_SIZE ) )
+ )
+ ),
+ Category.of(
+ get( KEY_UI_SKIN ),
+ Group.of(
+ get( KEY_UI_SKIN_SELECTION ),
+ Setting.of( label( KEY_UI_SKIN_SELECTION ) ),
+ Setting.of( title( KEY_UI_SKIN_SELECTION ),
+ skinListProperty(),
+ listProperty( KEY_UI_SKIN_SELECTION ) )
+ ),
+ Group.of(
+ get( KEY_UI_SKIN_CUSTOM ),
+ Setting.of( label( KEY_UI_SKIN_CUSTOM ) ),
+ Setting.of( title( KEY_UI_SKIN_CUSTOM ),
+ fileProperty( KEY_UI_SKIN_CUSTOM ), false )
+ )
+ ),
+ Category.of(
+ get( KEY_UI_PREVIEW ),
+ Group.of(
+ get( KEY_UI_PREVIEW_STYLESHEET ),
+ Setting.of( label( KEY_UI_PREVIEW_STYLESHEET ) ),
+ Setting.of( title( KEY_UI_PREVIEW_STYLESHEET ),
+ fileProperty( KEY_UI_PREVIEW_STYLESHEET ), false )
+ )
+ ),
+ Category.of(
+ get( KEY_LANGUAGE ),
+ Group.of(
+ get( KEY_LANGUAGE_LOCALE ),
+ Setting.of( label( KEY_LANGUAGE_LOCALE ) ),
+ Setting.of( title( KEY_LANGUAGE_LOCALE ),
+ localeListProperty(),
+ localeProperty( KEY_LANGUAGE_LOCALE ) )
+ )
+ )
+ };
+ }
+
+ @SuppressWarnings( "unchecked" )
+ private Setting<StringField, StringProperty> createMultilineSetting(
+ final String description, final Key property ) {
+ final Setting<StringField, StringProperty> setting =
+ Setting.of( description, stringProperty( property ) );
+ final var field = setting.getElement();
+ field.multiline( true );
+
+ return setting;
+ }
+
+ /**
+ * Map ENTER and ESCAPE keys to OK and CANCEL buttons, respectively.
+ */
+ private void initKeyEventHandler( final PreferencesFx preferences ) {
+ final var view = preferences.getView();
+ final var nodes = view.getChildrenUnmodifiable();
+ final var master = (MasterDetailPane) nodes.getFirst();
+ final var detail = (NavigationView) master.getDetailNode();
+ final var pane = (DialogPane) view.getParent();
+
+ detail.setOnKeyReleased( key -> {
+ switch( key.getCode() ) {
+ case ENTER -> ((Button) pane.lookupButton( OK )).fire();
+ case ESCAPE -> ((Button) pane.lookupButton( CANCEL )).fire();
+ default -> {}
+ }
+ } );
+ }
+
+ /**
+ * Called when the user clicks the APPLY or OK buttons in the dialog.
+ *
+ * @param preferences Preferences widget.
+ */
+ private void initSaveEventHandler( final PreferencesFx preferences ) {
+ preferences.addEventHandler(
+ EVENT_PREFERENCES_SAVED, _ -> mWorkspace.save()
+ );
+ }
+
+ /**
+ * Creates a label for the given key after interpolating its value.
+ *
+ * @param key The key to find in the resource bundle.
+ * @return The value of the key as a label.
+ */
+ private Node label( final Key key ) {
+ return label( key, (String[]) null );
+ }
+
+ private Node label( final Key key, final String... values ) {
+ return new Label( get( String.format( "%s%s", key.toString(), ".desc" ), (Object[]) values ) );
+ }
+
+ private String title( final Key key ) {
+ return get( String.format( "%s%s", key.toString(), ".title" ) );
+ }
+
+ /**
+ * Screens out non-existent directories to avoid throwing an exception caused
+ * by
+ * <a href="https://github.com/dlsc-software-consulting-gmbh/PreferencesFX/issues/441">
+ * PreferencesFX issue #441
+ * </a>.
+ *
+ * @param key Preference to pre-screen before creating a {@link FileProperty}.
+ * @return The preferred value or the user's home directory if the directory
+ * does not exist.
+ */
+ private ObjectProperty<File> directoryProperty( final Key key ) {
+ final var property = mWorkspace.fileProperty( key );
+ final var file = property.get();
+
+ if( !file.exists() ) {
+ property.set( USER_DIRECTORY );
+ }
+
+ return property;
+ }
+
+ private ObjectProperty<File> fileProperty( final Key key ) {
+ return mWorkspace.fileProperty( key );
+ }
+
+ private StringProperty stringProperty( final Key key ) {
+ return mWorkspace.stringProperty( key );
+ }
+
+ private BooleanProperty booleanProperty( final Key key ) {
+ return mWorkspace.booleanProperty( key );
+ }
+
+ private IntegerProperty integerProperty( final Key key ) {
+ return mWorkspace.integerProperty( key );
+ }
+
+ private DoubleProperty doubleProperty( final Key key ) {
+ return mWorkspace.doubleProperty( key );
+ }
+
+ private ObjectProperty<String> listProperty( final Key key ) {
+ return mWorkspace.listProperty( key );
}
src/main/java/com/keenwrite/preferences/SkinProperty.java
* Ordered set of available skins.
*/
- private static final Set<String> sSkins = new LinkedHashSet<>();
+ private static final Set<String> sProperties = new LinkedHashSet<>();
static {
- sSkins.add( "Count Darcula" );
- sSkins.add( "Haunted Grey" );
- sSkins.add( "Modena Dark" );
- sSkins.add( "Monokai" );
- sSkins.add( SKIN_DEFAULT );
- sSkins.add( "Silver Cavern" );
- sSkins.add( "Solarized Dark" );
- sSkins.add( "Vampire Byte" );
+ sProperties.add( "Count Darcula" );
+ sProperties.add( "Haunted Grey" );
+ sProperties.add( "Modena Dark" );
+ sProperties.add( "Monokai" );
+ sProperties.add( SKIN_DEFAULT );
+ sProperties.add( "Silver Cavern" );
+ sProperties.add( "Solarized Dark" );
+ sProperties.add( "Vampire Byte" );
+ }
+
+ public SkinProperty( final String skin ) {
+ super( skin );
}
/**
* Returns the list of available skin names to change the UI fonts and
* colours.
*
* @return A selection of skins.
*/
public static ObservableList<String> skinListProperty() {
- assert !sSkins.isEmpty();
+ assert !sProperties.isEmpty();
- return listProperty( sSkins );
+ return listProperty( sProperties );
}
/**
* Returns the given skin name as a sanitized file name, which must map
* to a stylesheet file bundled with the application. This does not include
* the path to the stylesheet. If the given name is not known, the file
* name for {@link Constants#SKIN_DEFAULT} is returned. The extension must
* be added separately.
*
- * @param skin The name to convert to a file name.
- * @return The given name converted lower case, spaces replaced with
+ * @param property The property name to convert to a file name.
+ * @return The given property name converted lower case, spaces replaced with
* underscores, without the ".css" extension appended.
*/
- public static String toFilename( final String skin ) {
- assert skin != null;
+ public static String toFilename( final String property ) {
+ assert property != null;
- return sanitize( skin ).toLowerCase().replace( ' ', '_' );
+ return sanitize( property ).toLowerCase().replace( ' ', '_' );
}
/**
- * Ensures that the given name is in the list of known skins.
+ * Ensures that the given property name is in the property list.
*
- * @param skin Validate this name's existence.
- * @return The given name, if valid, otherwise the default skin.
+ * @param property Property to validate.
+ * @return The given property was found, otherwise the default property.
*/
- private static String sanitize( final String skin ) {
- assert skin != null;
-
- return sSkins.contains( skin ) ? skin : SKIN_DEFAULT;
- }
+ private static String sanitize( final String property ) {
+ assert property != null;
- public SkinProperty( final String skin ) {
- super( skin );
+ return sProperties.contains( property ) ? property : SKIN_DEFAULT;
}
}
src/main/java/com/keenwrite/preferences/Workspace.java
entry( KEY_TYPESET_CONTEXT_THEME_SELECTION, asStringProperty( "boschet" ) ),
entry( KEY_TYPESET_CONTEXT_CHAPTERS, asStringProperty( "" ) ),
- entry( KEY_TYPESET_TYPOGRAPHY_QUOTES, asBooleanProperty( true ) ),
- entry( KEY_TYPESET_MODES_ENABLED, asStringProperty( "" ) )
- //@formatter:on
- );
-
- /**
- * Sets of configuration values, all the same type (e.g., file names),
- * where the key name doesn't change per set.
- */
- private final Map<Key, SetProperty<?>> mSets = Map.ofEntries(
- entry(
- KEY_UI_RECENT_OPEN_PATH,
- createSetProperty( new HashSet<String>() )
- )
- );
-
- /**
- * Lists of configuration values, such as key-value pairs where both the
- * key name and the value must be preserved per list.
- */
- private final Map<Key, ListProperty<?>> mLists = Map.ofEntries(
- entry(
- KEY_DOC_META,
- createListProperty( new LinkedList<Entry<String, String>>() )
- )
- );
-
- /**
- * Helps instantiate {@link Property} instances for XML configuration items.
- */
- private static final Map<Class<?>, Function<String, Object>> UNMARSHALL =
- Map.of(
- LocaleProperty.class, LocaleProperty::parseLocale,
- SimpleBooleanProperty.class, Boolean::parseBoolean,
- SimpleIntegerProperty.class, Integer::parseInt,
- SimpleDoubleProperty.class, Double::parseDouble,
- SimpleFloatProperty.class, Float::parseFloat,
- SimpleStringProperty.class, String::new,
- SimpleObjectProperty.class, String::new,
- SkinProperty.class, String::new,
- FileProperty.class, File::new
- );
-
- /**
- * The asymmetry with respect to {@link #UNMARSHALL} is because most objects
- * can simply call {@link Object#toString()} to convert the value to a string.
- */
- private static final Map<Class<?>, Function<String, Object>> MARSHALL =
- Map.of(
- LocaleProperty.class, LocaleProperty::toLanguageTag
- );
-
- /**
- * Converts the given {@link Property} value to a string.
- *
- * @param property The {@link Property} to convert.
- * @return A string representation of the given property, or the empty
- * string if no conversion was possible.
- */
- private static String marshall( final Property<?> property ) {
- final var v = property.getValue();
-
- return v == null
- ? ""
- : MARSHALL
- .getOrDefault( property.getClass(), _ -> property.getValue() )
- .apply( v.toString() )
- .toString();
- }
-
- private static Object unmarshall(
- final Property<?> property, final Object configValue ) {
- final var v = configValue.toString();
-
- return UNMARSHALL
- .getOrDefault( property.getClass(), _ -> property.getValue() )
- .apply( v );
- }
-
- /**
- * Creates an instance of {@link ObservableList} that is based on a
- * modifiable observable array list for the given items.
- *
- * @param items The items to wrap in an observable list.
- * @param <E> The type of items to add to the list.
- * @return An observable property that can have its contents modified.
- */
- public static <E> ObservableList<E> listProperty( final Set<E> items ) {
- return new SimpleListProperty<>( observableArrayList( items ) );
- }
-
- private static <E> SetProperty<E> createSetProperty( final Set<E> set ) {
- return new SimpleSetProperty<>( observableSet( set ) );
- }
-
- private static <E> ListProperty<E> createListProperty( final List<E> list ) {
- return new SimpleListProperty<>( observableArrayList( list ) );
- }
-
- private static StringProperty asStringProperty( final String value ) {
- return new SimpleStringProperty( value );
- }
-
- private static BooleanProperty asBooleanProperty() {
- return new SimpleBooleanProperty();
- }
-
- /**
- * @param value Default value.
- */
- @SuppressWarnings( "SameParameterValue" )
- private static BooleanProperty asBooleanProperty( final boolean value ) {
- return new SimpleBooleanProperty( value );
- }
-
- /**
- * @param value Default value.
- */
- @SuppressWarnings( "SameParameterValue" )
- private static IntegerProperty asIntegerProperty( final int value ) {
- return new SimpleIntegerProperty( value );
- }
-
- /**
- * @param value Default value.
- */
- private static DoubleProperty asDoubleProperty( final double value ) {
- return new SimpleDoubleProperty( value );
- }
-
- /**
- * @param value Default value.
- */
- private static FileProperty asFileProperty( final File value ) {
- return new FileProperty( value );
- }
-
- /**
- * @param value Default value.
- */
- @SuppressWarnings( "SameParameterValue" )
- private static LocaleProperty asLocaleProperty( final Locale value ) {
- return new LocaleProperty( value );
- }
-
- /**
- * @param value Default value.
- */
- @SuppressWarnings( "SameParameterValue" )
- private static SkinProperty asSkinProperty( final String value ) {
- return new SkinProperty( value );
- }
-
- /**
- * Creates a new {@link Workspace} that will attempt to load the users'
- * preferences. If the configuration file cannot be loaded, the workspace
- * settings returns default values.
- */
- public Workspace() {
- load();
- }
-
- /**
- * Attempts to load the app's configuration file.
- */
- private void load() {
- final var store = createXmlStore();
- store.load( FILE_PREFERENCES );
-
- mValues.keySet().forEach( key -> {
- try {
- final var storeValue = store.getValue( key );
- final var property = valuesProperty( key );
- final var unmarshalled = unmarshall( property, storeValue );
-
- property.setValue( unmarshalled );
- } catch( final NoSuchElementException ex ) {
- // When no configuration (item), use the default value.
- clue( ex );
- }
- } );
-
- mSets.keySet().forEach( key -> {
- final var set = store.getSet( key );
- final SetProperty<String> property = setsProperty( key );
-
- property.setValue( observableSet( set ) );
- } );
-
- mLists.keySet().forEach( key -> {
- final var map = store.getMap( key );
- final ListProperty<Entry<String, String>> property = listsProperty( key );
- final var list = map
- .entrySet()
- .stream()
- .toList();
-
- property.setValue( observableArrayList( list ) );
- } );
-
- WorkspaceLoadedEvent.fire( this );
- }
-
- /**
- * Saves the current workspace.
- */
- public void save() {
- final var store = createXmlStore();
-
- try {
- // Update the string values to include the application version.
- valuesProperty( KEY_META_VERSION ).setValue( getVersion() );
-
- mValues.forEach( ( k, v ) -> store.setValue( k, marshall( v ) ) );
- mSets.forEach( store::setSet );
- mLists.forEach( store::setMap );
-
- store.save( FILE_PREFERENCES );
- } catch( final Exception ex ) {
- clue( ex );
- }
- }
-
- /**
- * Returns a value that represents a setting in the application that the user
- * may configure, either directly or indirectly.
- *
- * @param key The reference to the users' preference stored in deference
- * of app reëntrance.
- * @return An observable property to be persisted.
- */
- @SuppressWarnings( "unchecked" )
- public <T, U extends Property<T>> U valuesProperty( final Key key ) {
- assert key != null;
- return (U) mValues.get( key );
- }
-
- /**
- * Returns a set of values that represent a setting in the application that
- * the user may configure, either directly or indirectly. The property
- * returned is backed by a {@link Set}.
- *
- * @param key The {@link Key} associated with a preference value.
- * @return An observable property to be persisted.
- */
- @SuppressWarnings( "unchecked" )
- public <T> SetProperty<T> setsProperty( final Key key ) {
- assert key != null;
- return (SetProperty<T>) mSets.get( key );
- }
-
- /**
- * Returns a list of values that represent a setting in the application that
- * the user may configure, either directly or indirectly. The property
- * returned is backed by a mutable {@link List}.
- *
- * @param key The {@link Key} associated with a preference value.
- * @return An observable property to be persisted.
- */
- @SuppressWarnings( "unchecked" )
- public <K, V> ListProperty<Entry<K, V>> listsProperty( final Key key ) {
- assert key != null;
- return (ListProperty<Entry<K, V>>) mLists.get( key );
- }
-
- /**
- * Returns the {@link String} {@link Property} associated with the given
- * {@link Key} from the internal list of preference values. The caller
- * must be sure that the given {@link Key} is associated with a {@link File}
- * {@link Property}.
- *
- * @param key The {@link Key} associated with a preference value.
- * @return The value associated with the given {@link Key}.
- */
- public StringProperty stringProperty( final Key key ) {
- assert key != null;
- return valuesProperty( key );
- }
-
- /**
- * Returns the {@link Boolean} {@link Property} associated with the given
- * {@link Key} from the internal list of preference values. The caller
- * must be sure that the given {@link Key} is associated with a {@link File}
- * {@link Property}.
- *
- * @param key The {@link Key} associated with a preference value.
- * @return The value associated with the given {@link Key}.
- */
- public BooleanProperty booleanProperty( final Key key ) {
- assert key != null;
- return valuesProperty( key );
- }
-
- /**
- * Returns the {@link Integer} {@link Property} associated with the given
- * {@link Key} from the internal list of preference values. The caller
- * must be sure that the given {@link Key} is associated with a {@link File}
- * {@link Property}.
- *
- * @param key The {@link Key} associated with a preference value.
- * @return The value associated with the given {@link Key}.
- */
- public IntegerProperty integerProperty( final Key key ) {
- assert key != null;
- return valuesProperty( key );
- }
-
- /**
- * Returns the {@link Double} {@link Property} associated with the given
- * {@link Key} from the internal list of preference values. The caller
- * must be sure that the given {@link Key} is associated with a {@link File}
- * {@link Property}.
- *
- * @param key The {@link Key} associated with a preference value.
- * @return The value associated with the given {@link Key}.
- */
- public DoubleProperty doubleProperty( final Key key ) {
- assert key != null;
- return valuesProperty( key );
- }
-
- /**
- * Returns the {@link File} {@link Property} associated with the given
- * {@link Key} from the internal list of preference values. The caller
- * must be sure that the given {@link Key} is associated with a {@link File}
- * {@link Property}.
- *
- * @param key The {@link Key} associated with a preference value.
- * @return The value associated with the given {@link Key}.
- */
- public ObjectProperty<File> fileProperty( final Key key ) {
- assert key != null;
- return valuesProperty( key );
- }
-
- /**
- * Returns the {@link Locale} {@link Property} associated with the given
- * {@link Key} from the internal list of preference values. The caller
- * must be sure that the given {@link Key} is associated with a {@link File}
- * {@link Property}.
- *
- * @param key The {@link Key} associated with a preference value.
- * @return The value associated with the given {@link Key}.
- */
- public LocaleProperty localeProperty( final Key key ) {
- assert key != null;
- return valuesProperty( key );
- }
-
- public ObjectProperty<String> skinProperty( final Key key ) {
+ entry( KEY_TYPESET_TYPOGRAPHY_QUOTES, asAposProperty( APOS_DEFAULT ) ),
+ entry( KEY_TYPESET_MODES_ENABLED, asStringProperty( "" ) )
+ //@formatter:on
+ );
+
+ /**
+ * Sets of configuration values, all the same type (e.g., file names),
+ * where the key name doesn't change per set.
+ */
+ private final Map<Key, SetProperty<?>> mSets = Map.ofEntries(
+ entry(
+ KEY_UI_RECENT_OPEN_PATH,
+ createSetProperty( new HashSet<String>() )
+ )
+ );
+
+ /**
+ * Lists of configuration values, such as key-value pairs where both the
+ * key name and the value must be preserved per list.
+ */
+ private final Map<Key, ListProperty<?>> mLists = Map.ofEntries(
+ entry(
+ KEY_DOC_META,
+ createListProperty( new LinkedList<Entry<String, String>>() )
+ )
+ );
+
+ /**
+ * Helps instantiate {@link Property} instances for XML configuration items.
+ */
+ private static final Map<Class<?>, Function<String, Object>> UNMARSHALL =
+ Map.of(
+ LocaleProperty.class, LocaleProperty::parseLocale,
+ SimpleBooleanProperty.class, Boolean::parseBoolean,
+ SimpleIntegerProperty.class, Integer::parseInt,
+ SimpleDoubleProperty.class, Double::parseDouble,
+ SimpleFloatProperty.class, Float::parseFloat,
+ SimpleStringProperty.class, String::new,
+ SimpleObjectProperty.class, String::new,
+ SkinProperty.class, String::new,
+ FileProperty.class, File::new
+ );
+
+ /**
+ * The asymmetry with respect to {@link #UNMARSHALL} is because most objects
+ * can simply call {@link Object#toString()} to convert the value to a string.
+ */
+ private static final Map<Class<?>, Function<String, Object>> MARSHALL =
+ Map.of(
+ LocaleProperty.class, LocaleProperty::toLanguageTag
+ );
+
+ /**
+ * Converts the given {@link Property} value to a string.
+ *
+ * @param property The {@link Property} to convert.
+ * @return A string representation of the given property, or the empty
+ * string if no conversion was possible.
+ */
+ private static String marshall( final Property<?> property ) {
+ final var v = property.getValue();
+
+ return v == null
+ ? ""
+ : MARSHALL
+ .getOrDefault( property.getClass(), _ -> property.getValue() )
+ .apply( v.toString() )
+ .toString();
+ }
+
+ private static Object unmarshall(
+ final Property<?> property, final Object configValue ) {
+ final var v = configValue.toString();
+
+ return UNMARSHALL
+ .getOrDefault( property.getClass(), _ -> property.getValue() )
+ .apply( v );
+ }
+
+ /**
+ * Creates an instance of {@link ObservableList} that is based on a
+ * modifiable observable array list for the given items.
+ *
+ * @param items The items to wrap in an observable list.
+ * @param <E> The type of items to add to the list.
+ * @return An observable property that can have its contents modified.
+ */
+ public static <E> ObservableList<E> listProperty( final Set<E> items ) {
+ return new SimpleListProperty<>( observableArrayList( items ) );
+ }
+
+ private static <E> SetProperty<E> createSetProperty( final Set<E> set ) {
+ return new SimpleSetProperty<>( observableSet( set ) );
+ }
+
+ private static <E> ListProperty<E> createListProperty( final List<E> list ) {
+ return new SimpleListProperty<>( observableArrayList( list ) );
+ }
+
+ private static StringProperty asStringProperty( final String value ) {
+ return new SimpleStringProperty( value );
+ }
+
+ private static BooleanProperty asBooleanProperty() {
+ return new SimpleBooleanProperty();
+ }
+
+ /**
+ * @param value Default value.
+ */
+ @SuppressWarnings( "SameParameterValue" )
+ private static BooleanProperty asBooleanProperty( final boolean value ) {
+ return new SimpleBooleanProperty( value );
+ }
+
+ /**
+ * @param value Default value.
+ */
+ @SuppressWarnings( "SameParameterValue" )
+ private static IntegerProperty asIntegerProperty( final int value ) {
+ return new SimpleIntegerProperty( value );
+ }
+
+ /**
+ * @param value Default value.
+ */
+ private static DoubleProperty asDoubleProperty( final double value ) {
+ return new SimpleDoubleProperty( value );
+ }
+
+ /**
+ * @param value Default value.
+ */
+ private static FileProperty asFileProperty( final File value ) {
+ return new FileProperty( value );
+ }
+
+ /**
+ * @param value Default value.
+ */
+ @SuppressWarnings( "SameParameterValue" )
+ private static LocaleProperty asLocaleProperty( final Locale value ) {
+ return new LocaleProperty( value );
+ }
+
+ /**
+ * @param value Default value.
+ */
+ @SuppressWarnings( "SameParameterValue" )
+ private static SkinProperty asSkinProperty( final String value ) {
+ return new SkinProperty( value );
+ }
+
+ /**
+ * @param value Default value.
+ */
+ @SuppressWarnings( "SameParameterValue" )
+ private static AposProperty asAposProperty( final String value ) {
+ return new AposProperty( value );
+ }
+
+ /**
+ * Creates a new {@link Workspace} that will attempt to load the users'
+ * preferences. If the configuration file cannot be loaded, the workspace
+ * settings returns default values.
+ */
+ public Workspace() {
+ load();
+ }
+
+ /**
+ * Attempts to load the app's configuration file.
+ */
+ private void load() {
+ final var store = createXmlStore();
+ store.load( FILE_PREFERENCES );
+
+ mValues.keySet().forEach( key -> {
+ try {
+ final var storeValue = store.getValue( key );
+ final var property = valuesProperty( key );
+ final var unmarshalled = unmarshall( property, storeValue );
+
+ property.setValue( unmarshalled );
+ } catch( final NoSuchElementException ex ) {
+ // When no configuration (item), use the default value.
+ clue( ex );
+ }
+ } );
+
+ mSets.keySet().forEach( key -> {
+ final var set = store.getSet( key );
+ final SetProperty<String> property = setsProperty( key );
+
+ property.setValue( observableSet( set ) );
+ } );
+
+ mLists.keySet().forEach( key -> {
+ final var map = store.getMap( key );
+ final ListProperty<Entry<String, String>> property = listsProperty( key );
+ final var list = map
+ .entrySet()
+ .stream()
+ .toList();
+
+ property.setValue( observableArrayList( list ) );
+ } );
+
+ WorkspaceLoadedEvent.fire( this );
+ }
+
+ /**
+ * Saves the current workspace.
+ */
+ public void save() {
+ final var store = createXmlStore();
+
+ try {
+ // Update the string values to include the application version.
+ valuesProperty( KEY_META_VERSION ).setValue( getVersion() );
+
+ mValues.forEach( ( k, v ) -> store.setValue( k, marshall( v ) ) );
+ mSets.forEach( store::setSet );
+ mLists.forEach( store::setMap );
+
+ store.save( FILE_PREFERENCES );
+ } catch( final Exception ex ) {
+ clue( ex );
+ }
+ }
+
+ /**
+ * Returns a value that represents a setting in the application that the user
+ * may configure, either directly or indirectly.
+ *
+ * @param key The reference to the users' preference stored in deference
+ * of app reëntrance.
+ * @return An observable property to be persisted.
+ */
+ @SuppressWarnings( "unchecked" )
+ public <T, U extends Property<T>> U valuesProperty( final Key key ) {
+ assert key != null;
+ return (U) mValues.get( key );
+ }
+
+ /**
+ * Returns a set of values that represent a setting in the application that
+ * the user may configure, either directly or indirectly. The property
+ * returned is backed by a {@link Set}.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return An observable property to be persisted.
+ */
+ @SuppressWarnings( "unchecked" )
+ public <T> SetProperty<T> setsProperty( final Key key ) {
+ assert key != null;
+ return (SetProperty<T>) mSets.get( key );
+ }
+
+ /**
+ * Returns a list of values that represent a setting in the application that
+ * the user may configure, either directly or indirectly. The property
+ * returned is backed by a mutable {@link List}.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return An observable property to be persisted.
+ */
+ @SuppressWarnings( "unchecked" )
+ public <K, V> ListProperty<Entry<K, V>> listsProperty( final Key key ) {
+ assert key != null;
+ return (ListProperty<Entry<K, V>>) mLists.get( key );
+ }
+
+ /**
+ * Returns the {@link String} {@link Property} associated with the given
+ * {@link Key} from the internal list of preference values. The caller
+ * must be sure that the given {@link Key} is associated with a {@link File}
+ * {@link Property}.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return The value associated with the given {@link Key}.
+ */
+ public StringProperty stringProperty( final Key key ) {
+ assert key != null;
+ return valuesProperty( key );
+ }
+
+ /**
+ * Returns the {@link Boolean} {@link Property} associated with the given
+ * {@link Key} from the internal list of preference values. The caller
+ * must be sure that the given {@link Key} is associated with a {@link File}
+ * {@link Property}.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return The value associated with the given {@link Key}.
+ */
+ public BooleanProperty booleanProperty( final Key key ) {
+ assert key != null;
+ return valuesProperty( key );
+ }
+
+ /**
+ * Returns the {@link Integer} {@link Property} associated with the given
+ * {@link Key} from the internal list of preference values. The caller
+ * must be sure that the given {@link Key} is associated with a {@link File}
+ * {@link Property}.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return The value associated with the given {@link Key}.
+ */
+ public IntegerProperty integerProperty( final Key key ) {
+ assert key != null;
+ return valuesProperty( key );
+ }
+
+ /**
+ * Returns the {@link Double} {@link Property} associated with the given
+ * {@link Key} from the internal list of preference values. The caller
+ * must be sure that the given {@link Key} is associated with a {@link File}
+ * {@link Property}.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return The value associated with the given {@link Key}.
+ */
+ public DoubleProperty doubleProperty( final Key key ) {
+ assert key != null;
+ return valuesProperty( key );
+ }
+
+ /**
+ * Returns the {@link File} {@link Property} associated with the given
+ * {@link Key} from the internal list of preference values. The caller
+ * must be sure that the given {@link Key} is associated with a {@link File}
+ * {@link Property}.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return The value associated with the given {@link Key}.
+ */
+ public ObjectProperty<File> fileProperty( final Key key ) {
+ assert key != null;
+ return valuesProperty( key );
+ }
+
+ /**
+ * Returns the {@link Locale} {@link Property} associated with the given
+ * {@link Key} from the internal list of preference values. The caller
+ * must be sure that the given {@link Key} is associated with a {@link File}
+ * {@link Property}.
+ *
+ * @param key The {@link Key} associated with a preference value.
+ * @return The value associated with the given {@link Key}.
+ */
+ public LocaleProperty localeProperty( final Key key ) {
+ assert key != null;
+ return valuesProperty( key );
+ }
+
+ public ObjectProperty<String> listProperty( final Key key ) {
assert key != null;
return valuesProperty( key );
src/main/java/com/keenwrite/processors/ProcessorContext.java
private Supplier<String> mRScript = () -> "";
- private Supplier<Boolean> mCurlQuotes = () -> true;
- private Supplier<Boolean> mAutoRemove = () -> true;
-
- public void setSourcePath( final Path sourcePath ) {
- assert sourcePath != null;
- mSourcePath = sourcePath;
- }
-
- public void setTargetPath( final Path outputPath ) {
- assert outputPath != null;
- mTargetPath = outputPath;
- }
-
- public void setThemeDir( final Supplier<Path> themeDir ) {
- assert themeDir != null;
- mThemeDir = themeDir;
- }
-
- public void setCacheDir( final Supplier<File> cacheDir ) {
- assert cacheDir != null;
-
- mCacheDir = () -> {
- final var dir = cacheDir.get();
-
- return (dir == null ? toFile( USER_DATA_DIR ) : dir).toPath();
- };
- }
-
- public void setImageDir( final Supplier<File> imageDir ) {
- assert imageDir != null;
-
- mImageDir = () -> {
- final var dir = imageDir.get();
-
- return (dir == null ? USER_DIRECTORY : dir).toPath();
- };
- }
-
- public void setImageOrder( final Supplier<String> imageOrder ) {
- assert imageOrder != null;
- mImageOrder = imageOrder;
- }
-
- public void setImageServer( final Supplier<String> imageServer ) {
- assert imageServer != null;
- mImageServer = imageServer;
- }
-
- public void setFontDir( final Supplier<File> fontDir ) {
- assert fontDir != null;
-
- mFontDir = () -> {
- final var dir = fontDir.get();
-
- return (dir == null ? USER_DIRECTORY : dir).toPath();
- };
- }
-
- public void setModesEnabled( final Supplier<String> modesEnabled ) {
- assert modesEnabled != null;
- mModesEnabled = modesEnabled;
- }
-
- public void setExportFormat( final ExportFormat exportFormat ) {
- assert exportFormat != null;
- mExportFormat = exportFormat;
- }
-
- public void setConcatenate( final Supplier<Boolean> concatenate ) {
- mConcatenate = concatenate;
- }
-
- public void setChapters( final Supplier<String> chapters ) {
- mChapters = chapters;
- }
-
- public void setLocale( final Supplier<Locale> locale ) {
- assert locale != null;
- mLocale = locale;
- }
-
- /**
- * Sets the list of fully interpolated key-value pairs to use when
- * substituting variable names back into the document as variable values.
- * This uses a {@link Callable} reference so that GUI and command-line
- * usage can insert their respective behaviours. That is, this method
- * prevents coupling the GUI to the CLI.
- *
- * @param supplier Defines how to retrieve the definitions.
- */
- public void setDefinitions( final Supplier<Map<String, String>> supplier ) {
- assert supplier != null;
- mDefinitions = supplier;
- }
-
- /**
- * Sets metadata to use in the document header. These are made available
- * to the typesetting engine as {@code \documentvariable} values.
- *
- * @param metadata The key/value pairs to publish as document metadata.
- */
- public void setMetadata( final Supplier<Map<String, String>> metadata ) {
- assert metadata != null;
- mMetadata = metadata.get() == null ? HashMap::new : metadata;
- }
-
- /**
- * Sets document variables to use when building the document. These
- * variables will override existing key/value pairs, or be added as
- * new key/value pairs if not already defined. This allows users to
- * inject variables into the document from the command-line, allowing
- * for dynamic assignment of in-text values when building documents.
- *
- * @param overrides The key/value pairs to add (or override) as variables.
- */
- public void setOverrides( final Supplier<Map<String, String>> overrides ) {
- assert overrides != null;
- assert mDefinitions != null;
- assert mDefinitions.get() != null;
-
- final var map = overrides.get();
-
- if( map != null ) {
- mDefinitions.get().putAll( map );
- }
- }
-
- /**
- * Sets the source for deriving the {@link Caret}. Typically, this is
- * the text editor that has focus.
- *
- * @param caret The source for the currently active caret.
- */
- public void setCaret( final Supplier<Caret> caret ) {
- assert caret != null;
- mCaret = caret;
- }
-
- public void setSigilBegan( final Supplier<String> sigilBegan ) {
- assert sigilBegan != null;
- mSigilBegan = sigilBegan;
- }
-
- public void setSigilEnded( final Supplier<String> sigilEnded ) {
- assert sigilEnded != null;
- mSigilEnded = sigilEnded;
- }
-
- public void setRWorkingDir( final Supplier<Path> rWorkingDir ) {
- assert rWorkingDir != null;
- mRWorkingDir = rWorkingDir;
- }
-
- public void setRScript( final Supplier<String> rScript ) {
- assert rScript != null;
- mRScript = rScript;
- }
-
- public void setCurlQuotes( final Supplier<Boolean> curlQuotes ) {
- assert curlQuotes != null;
- mCurlQuotes = curlQuotes;
- }
-
- public void setAutoRemove( final Supplier<Boolean> autoRemove ) {
- assert autoRemove != null;
- mAutoRemove = autoRemove;
- }
-
- private boolean isExportFormat( final ExportFormat format ) {
- return mExportFormat == format;
- }
- }
-
- public static GenericBuilder<Mutator, ProcessorContext> builder() {
- return GenericBuilder.of( Mutator::new, ProcessorContext::new );
- }
-
- /**
- * Creates a new context for use by the {@link ProcessorFactory} when
- * instantiating new {@link Processor} instances. Although all the
- * parameters are required, not all {@link Processor} instances will use
- * all parameters.
- */
- private ProcessorContext( final Mutator mutator ) {
- assert mutator != null;
-
- mMutator = mutator;
- }
-
- public Path getSourcePath() {
- return mMutator.mSourcePath;
- }
-
- /**
- * Answers what type of input document is to be processed.
- *
- * @return The input document's {@link MediaType}.
- */
- public MediaType getSourceType() {
- return MediaTypeExtension.fromPath( mMutator.mSourcePath );
- }
-
- /**
- * Fully qualified file name to use when exporting (e.g., document.pdf).
- *
- * @return Full path to a file name.
- */
- public Path getTargetPath() {
- return mMutator.mTargetPath;
- }
-
- public ExportFormat getExportFormat() {
- return mMutator.mExportFormat;
- }
-
- public Locale getLocale() {
- return mMutator.mLocale.get();
- }
-
- /**
- * Returns the variable map of definitions, without interpolation.
- *
- * @return A map to help dereference variables.
- */
- public Map<String, String> getDefinitions() {
- return mMutator.mDefinitions.get();
- }
-
- /**
- * Returns the variable map of definitions, with interpolation.
- *
- * @return A map to help dereference variables.
- */
- public InterpolatingMap getInterpolatedDefinitions() {
- return new InterpolatingMap(
- createDefinitionKeyOperator(), getDefinitions()
- ).interpolate();
- }
-
- public Map<String, String> getMetadata() {
- return mMutator.mMetadata.get();
- }
-
- /**
- * Returns the current caret position in the document being edited and is
- * always up-to-date.
- *
- * @return Caret position in the document.
- */
- public Supplier<Caret> getCaret() {
- return mMutator.mCaret;
- }
-
- /**
- * Returns the directory that contains the file being edited. When
- * {@link Constants#DOCUMENT_DEFAULT} is created, the parent path is
- * {@code null}. This will get absolute path to the file before trying to
- * get te parent path, which should always be a valid path. In the unlikely
- * event that the base path cannot be determined by the path alone, the
- * default user directory is returned. This is necessary for the creation
- * of new files.
- *
- * @return Path to the directory containing a file being edited, or the
- * default user directory if the base path cannot be determined.
- */
- public Path getBaseDir() {
- final var path = getSourcePath().toAbsolutePath().getParent();
- return path == null ? DEFAULT_DIRECTORY : path;
- }
-
- FileType getSourceFileType() {
- return lookup( getSourcePath() );
- }
-
- public Path getThemeDir() {
- return mMutator.mThemeDir.get();
- }
-
- public Path getImageDir() {
- return mMutator.mImageDir.get();
- }
-
- public Path getCacheDir() {
- return mMutator.mCacheDir.get();
- }
-
- public Iterable<String> getImageOrder() {
- assert mMutator.mImageOrder != null;
-
- final var order = mMutator.mImageOrder.get();
- final var token = order.contains( "," ) ? ',' : ' ';
-
- return Splitter.on( token ).split( token + order );
- }
-
- public String getImageServer() {
- return mMutator.mImageServer.get();
- }
-
- public Path getFontDir() {
- return mMutator.mFontDir.get();
- }
-
- public String getModesEnabled() {
- // Force the processor to select particular sigils.
- final var processor = new VariableProcessor( IDENTITY, this );
- final var needles = processor.getDefinitions();
- final var haystack = sanitize( mMutator.mModesEnabled.get() );
-
- return needles.containsKey( haystack )
- ? replace( haystack, needles )
- : processor.hasSigils( haystack )
- ? ""
- : haystack;
- }
-
- public boolean getAutoRemove() {
- return mMutator.mAutoRemove.get();
- }
-
- public Path getRWorkingDir() {
- return mMutator.mRWorkingDir.get();
- }
-
- public String getRScript() {
- return mMutator.mRScript.get();
- }
-
- public boolean getCurlQuotes() {
- return mMutator.mCurlQuotes.get();
+ private Supplier<String> mCurlQuotes = () -> APOS_DEFAULT;
+ private Supplier<Boolean> mAutoRemove = () -> true;
+
+ public void setSourcePath( final Path sourcePath ) {
+ assert sourcePath != null;
+ mSourcePath = sourcePath;
+ }
+
+ public void setTargetPath( final Path outputPath ) {
+ assert outputPath != null;
+ mTargetPath = outputPath;
+ }
+
+ public void setThemeDir( final Supplier<Path> themeDir ) {
+ assert themeDir != null;
+ mThemeDir = themeDir;
+ }
+
+ public void setCacheDir( final Supplier<File> cacheDir ) {
+ assert cacheDir != null;
+
+ mCacheDir = () -> {
+ final var dir = cacheDir.get();
+
+ return (dir == null ? toFile( USER_DATA_DIR ) : dir).toPath();
+ };
+ }
+
+ public void setImageDir( final Supplier<File> imageDir ) {
+ assert imageDir != null;
+
+ mImageDir = () -> {
+ final var dir = imageDir.get();
+
+ return (dir == null ? USER_DIRECTORY : dir).toPath();
+ };
+ }
+
+ public void setImageOrder( final Supplier<String> imageOrder ) {
+ assert imageOrder != null;
+ mImageOrder = imageOrder;
+ }
+
+ public void setImageServer( final Supplier<String> imageServer ) {
+ assert imageServer != null;
+ mImageServer = imageServer;
+ }
+
+ public void setFontDir( final Supplier<File> fontDir ) {
+ assert fontDir != null;
+
+ mFontDir = () -> {
+ final var dir = fontDir.get();
+
+ return (dir == null ? USER_DIRECTORY : dir).toPath();
+ };
+ }
+
+ public void setModesEnabled( final Supplier<String> modesEnabled ) {
+ assert modesEnabled != null;
+ mModesEnabled = modesEnabled;
+ }
+
+ public void setExportFormat( final ExportFormat exportFormat ) {
+ assert exportFormat != null;
+ mExportFormat = exportFormat;
+ }
+
+ public void setConcatenate( final Supplier<Boolean> concatenate ) {
+ mConcatenate = concatenate;
+ }
+
+ public void setChapters( final Supplier<String> chapters ) {
+ mChapters = chapters;
+ }
+
+ public void setLocale( final Supplier<Locale> locale ) {
+ assert locale != null;
+ mLocale = locale;
+ }
+
+ /**
+ * Sets the list of fully interpolated key-value pairs to use when
+ * substituting variable names back into the document as variable values.
+ * This uses a {@link Callable} reference so that GUI and command-line
+ * usage can insert their respective behaviours. That is, this method
+ * prevents coupling the GUI to the CLI.
+ *
+ * @param supplier Defines how to retrieve the definitions.
+ */
+ public void setDefinitions( final Supplier<Map<String, String>> supplier ) {
+ assert supplier != null;
+ mDefinitions = supplier;
+ }
+
+ /**
+ * Sets metadata to use in the document header. These are made available
+ * to the typesetting engine as {@code \documentvariable} values.
+ *
+ * @param metadata The key/value pairs to publish as document metadata.
+ */
+ public void setMetadata( final Supplier<Map<String, String>> metadata ) {
+ assert metadata != null;
+ mMetadata = metadata.get() == null ? HashMap::new : metadata;
+ }
+
+ /**
+ * Sets document variables to use when building the document. These
+ * variables will override existing key/value pairs, or be added as
+ * new key/value pairs if not already defined. This allows users to
+ * inject variables into the document from the command-line, allowing
+ * for dynamic assignment of in-text values when building documents.
+ *
+ * @param overrides The key/value pairs to add (or override) as variables.
+ */
+ public void setOverrides( final Supplier<Map<String, String>> overrides ) {
+ assert overrides != null;
+ assert mDefinitions != null;
+ assert mDefinitions.get() != null;
+
+ final var map = overrides.get();
+
+ if( map != null ) {
+ mDefinitions.get().putAll( map );
+ }
+ }
+
+ /**
+ * Sets the source for deriving the {@link Caret}. Typically, this is
+ * the text editor that has focus.
+ *
+ * @param caret The source for the currently active caret.
+ */
+ public void setCaret( final Supplier<Caret> caret ) {
+ assert caret != null;
+ mCaret = caret;
+ }
+
+ public void setSigilBegan( final Supplier<String> sigilBegan ) {
+ assert sigilBegan != null;
+ mSigilBegan = sigilBegan;
+ }
+
+ public void setSigilEnded( final Supplier<String> sigilEnded ) {
+ assert sigilEnded != null;
+ mSigilEnded = sigilEnded;
+ }
+
+ public void setRWorkingDir( final Supplier<Path> rWorkingDir ) {
+ assert rWorkingDir != null;
+ mRWorkingDir = rWorkingDir;
+ }
+
+ public void setRScript( final Supplier<String> rScript ) {
+ assert rScript != null;
+ mRScript = rScript;
+ }
+
+ public void setCurlQuotes( final Supplier<String> encoding ) {
+ assert encoding != null;
+ mCurlQuotes = encoding;
+ }
+
+ public void setAutoRemove( final Supplier<Boolean> autoRemove ) {
+ assert autoRemove != null;
+ mAutoRemove = autoRemove;
+ }
+
+ private boolean isExportFormat( final ExportFormat format ) {
+ return mExportFormat == format;
+ }
+ }
+
+ public static GenericBuilder<Mutator, ProcessorContext> builder() {
+ return GenericBuilder.of( Mutator::new, ProcessorContext::new );
+ }
+
+ /**
+ * Creates a new context for use by the {@link ProcessorFactory} when
+ * instantiating new {@link Processor} instances. Although all the
+ * parameters are required, not all {@link Processor} instances will use
+ * all parameters.
+ */
+ private ProcessorContext( final Mutator mutator ) {
+ assert mutator != null;
+
+ mMutator = mutator;
+ }
+
+ public Path getSourcePath() {
+ return mMutator.mSourcePath;
+ }
+
+ /**
+ * Answers what type of input document is to be processed.
+ *
+ * @return The input document's {@link MediaType}.
+ */
+ public MediaType getSourceType() {
+ return MediaTypeExtension.fromPath( mMutator.mSourcePath );
+ }
+
+ /**
+ * Fully qualified file name to use when exporting (e.g., document.pdf).
+ *
+ * @return Full path to a file name.
+ */
+ public Path getTargetPath() {
+ return mMutator.mTargetPath;
+ }
+
+ public ExportFormat getExportFormat() {
+ return mMutator.mExportFormat;
+ }
+
+ public Locale getLocale() {
+ return mMutator.mLocale.get();
+ }
+
+ /**
+ * Returns the variable map of definitions, without interpolation.
+ *
+ * @return A map to help dereference variables.
+ */
+ public Map<String, String> getDefinitions() {
+ return mMutator.mDefinitions.get();
+ }
+
+ /**
+ * Returns the variable map of definitions, with interpolation.
+ *
+ * @return A map to help dereference variables.
+ */
+ public InterpolatingMap getInterpolatedDefinitions() {
+ return new InterpolatingMap(
+ createDefinitionKeyOperator(), getDefinitions()
+ ).interpolate();
+ }
+
+ public Map<String, String> getMetadata() {
+ return mMutator.mMetadata.get();
+ }
+
+ /**
+ * Returns the current caret position in the document being edited and is
+ * always up-to-date.
+ *
+ * @return Caret position in the document.
+ */
+ public Supplier<Caret> getCaret() {
+ return mMutator.mCaret;
+ }
+
+ /**
+ * Returns the directory that contains the file being edited. When
+ * {@link Constants#DOCUMENT_DEFAULT} is created, the parent path is
+ * {@code null}. This will get absolute path to the file before trying to
+ * get te parent path, which should always be a valid path. In the unlikely
+ * event that the base path cannot be determined by the path alone, the
+ * default user directory is returned. This is necessary for the creation
+ * of new files.
+ *
+ * @return Path to the directory containing a file being edited, or the
+ * default user directory if the base path cannot be determined.
+ */
+ public Path getBaseDir() {
+ final var path = getSourcePath().toAbsolutePath().getParent();
+ return path == null ? DEFAULT_DIRECTORY : path;
+ }
+
+ FileType getSourceFileType() {
+ return lookup( getSourcePath() );
+ }
+
+ public Path getThemeDir() {
+ return mMutator.mThemeDir.get();
+ }
+
+ public Path getImageDir() {
+ return mMutator.mImageDir.get();
+ }
+
+ public Path getCacheDir() {
+ return mMutator.mCacheDir.get();
+ }
+
+ public Iterable<String> getImageOrder() {
+ assert mMutator.mImageOrder != null;
+
+ final var order = mMutator.mImageOrder.get();
+ final var token = order.contains( "," ) ? ',' : ' ';
+
+ return Splitter.on( token ).split( token + order );
+ }
+
+ public String getImageServer() {
+ return mMutator.mImageServer.get();
+ }
+
+ public Path getFontDir() {
+ return mMutator.mFontDir.get();
+ }
+
+ public String getModesEnabled() {
+ // Force the processor to select particular sigils.
+ final var processor = new VariableProcessor( IDENTITY, this );
+ final var needles = processor.getDefinitions();
+ final var haystack = sanitize( mMutator.mModesEnabled.get() );
+
+ return needles.containsKey( haystack )
+ ? replace( haystack, needles )
+ : processor.hasSigils( haystack )
+ ? ""
+ : haystack;
+ }
+
+ public boolean getAutoRemove() {
+ return mMutator.mAutoRemove.get();
+ }
+
+ public Path getRWorkingDir() {
+ return mMutator.mRWorkingDir.get();
+ }
+
+ public String getRScript() {
+ return mMutator.mRScript.get();
+ }
+
+ public String getCurlQuotes() {
+ final var result = mMutator.mCurlQuotes.get();
+
+ return "true".equalsIgnoreCase( result ) ? APOS_DEFAULT : result;
}
src/main/java/com/keenwrite/processors/html/HtmlPreviewProcessor.java
import com.keenwrite.processors.ExecutorProcessor;
import com.keenwrite.processors.ProcessorContext;
+import com.whitemagicsoftware.keenquotes.parser.Apostrophe;
import com.whitemagicsoftware.keenquotes.parser.Curler;
import static com.keenwrite.processors.html.Configuration.createCurler;
import static com.whitemagicsoftware.keenquotes.lex.FilterType.FILTER_XML;
-import static com.whitemagicsoftware.keenquotes.parser.Apostrophe.CONVERT_RSQUOTE;
/**
* Responsible for notifying the {@link HtmlPreview} when the succession
* chain has updated. This decouples knowledge of changes to the editor panel
* from the HTML preview panel as well as any processing that takes place
* before the final HTML preview is rendered. This is the last link in the
* processor chain.
*/
public final class HtmlPreviewProcessor extends ExecutorProcessor<String> {
- private static final Curler CURLER = createCurler(
- FILTER_XML, CONVERT_RSQUOTE
+ /**
+ * Force the straight quotes to be curled to \&rsquo; in the preview.
+ */
+ private final static Curler CURLER = createCurler(
+ FILTER_XML, Apostrophe.CONVERT_RSQUOTE
);
public String apply( final String html ) {
assert html != null;
- final var result = mContext.getCurlQuotes() ? CURLER.apply( html ) : html;
- sPreview.render( result );
+ final var apos = mContext.getCurlQuotes();
+ final var document = apos.isBlank() ? html : CURLER.apply( html );
+
+ sPreview.render( document );
return html;
}
src/main/java/com/keenwrite/processors/html/HtmlProcessor.java
import com.keenwrite.processors.Processor;
import com.keenwrite.processors.ProcessorContext;
-import com.whitemagicsoftware.keenquotes.parser.Curler;
+import com.whitemagicsoftware.keenquotes.parser.Apostrophe;
import static com.keenwrite.processors.html.Configuration.createCurler;
import static com.whitemagicsoftware.keenquotes.lex.FilterType.FILTER_XML;
-import static com.whitemagicsoftware.keenquotes.parser.Apostrophe.CONVERT_RSQUOTE_HEX;
/**
* This is the processor used when an HTML file name extension is encountered.
*/
public final class HtmlProcessor extends ExecutorProcessor<String> {
- private static final Curler CURLER = createCurler(
- FILTER_XML, CONVERT_RSQUOTE_HEX
- );
-
private final ProcessorContext mContext;
@Override
public String apply( final String t ) {
- return mContext.getCurlQuotes() ? CURLER.apply( t ) : t;
+ final var curl = mContext.getCurlQuotes();
+ final var curler = createCurler(
+ FILTER_XML, Apostrophe.fromType( curl )
+ );
+
+ return curler.apply( t );
}
}
src/main/java/com/keenwrite/processors/html/XhtmlProcessor.java
import com.keenwrite.ui.heuristics.WordCounter;
import com.keenwrite.util.DataTypeConverter;
+import com.whitemagicsoftware.keenquotes.parser.Apostrophe;
import com.whitemagicsoftware.keenquotes.parser.Contractions;
-import com.whitemagicsoftware.keenquotes.parser.Curler;
import org.w3c.dom.Document;
import static com.keenwrite.util.ProtocolScheme.getProtocol;
import static com.whitemagicsoftware.keenquotes.lex.FilterType.FILTER_XML;
-import static com.whitemagicsoftware.keenquotes.parser.Apostrophe.CONVERT_APOS;
import static java.lang.String.format;
import static java.lang.String.valueOf;
-import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.copy;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
/**
* Responsible for making an XHTML document complete by wrapping it with html
* and body elements. This doesn't have to be super-efficient because it's
* not run in real time.
*/
public final class XhtmlProcessor extends ExecutorProcessor<String> {
- private static final Curler CURLER = createCurler( FILTER_XML, CONVERT_APOS );
private static final String DTD =
"<!DOCTYPE html PUBLIC " +
} );
- final var locale = mContext.getLocale();
final var metadata = createMetaDataMap( doc );
- final var xhtml = DocumentParser.create( doc, metadata, locale );
+ final var title = metadata.get( "document.title" );
+ final var locale = mContext.getLocale();
+ final var xhtml = DocumentParser.create( doc, metadata, locale, title );
final var document = DTD + DocumentParser.toString( xhtml );
final var curl = mContext.getCurlQuotes();
+ final var curler = createCurler( FILTER_XML, Apostrophe.fromType( curl ) );
- return curl ? CURLER.apply( document ) : document;
+ return curler.apply( document );
} catch( final Exception ex ) {
clue( ex );
}
return html;
- }
-
- /**
- * Applies the metadata fields to the document.
- *
- * @param doc The document to adorn with metadata.
- */
- private void setMetaData( final Document doc ) {
- final var metadata = createMetaDataMap( doc );
- final var title = metadata.get( "title" );
-
- visit( doc, "/html/head", node -> {
- // Insert <title>text</title> inside <head>.
- node.appendChild( createElement( doc, "title", title ) );
- // Insert <meta charset="utf-8"> inside <head>.
- node.appendChild( createEncoding( doc, UTF_8.toString() ) );
-
- // Insert each <meta name=x content=y /> inside <head>.
- metadata.entrySet().forEach(
- entry -> node.appendChild( createMeta( doc, entry ) )
- );
- } );
}
src/main/resources/com/keenwrite/messages.properties
workspace.typeset.typography=Typography
workspace.typeset.typography.quotes=Quotation Marks
-workspace.typeset.typography.quotes.desc=Export straight quotes and apostrophes as curled equivalents.
+workspace.typeset.typography.quotes.desc=Defines encoding for apostrophes when curling quotation marks (regular means none; apos is typical).
workspace.typeset.typography.quotes.title=Curl
workspace.typeset.modes=Modes
src/test/java/com/keenwrite/processors/html/XhtmlProcessorTest.java
import static com.keenwrite.ExportFormat.HTML_TEX_DELIMITED;
import static com.keenwrite.ExportFormat.XHTML_TEX;
+import static com.keenwrite.constants.Constants.APOS_DEFAULT;
import static com.keenwrite.processors.ProcessorContext.builder;
import static com.keenwrite.processors.ProcessorFactory.createProcessors;
.with( ProcessorContext.Mutator::setRScript, () -> "" )
.with( ProcessorContext.Mutator::setRWorkingDir, () -> Path.of( "r" ) )
- .with( ProcessorContext.Mutator::setCurlQuotes, () -> true )
+ .with( ProcessorContext.Mutator::setCurlQuotes, () -> APOS_DEFAULT )
.with( ProcessorContext.Mutator::setAutoRemove, () -> true )
.build();

Separates GUI quotes from CLI quotes

Author DaveJarvis <email>
Date 2025-08-21 21:34:51 GMT-0700
Commit e8d081ee2f7a1d9ed5ec25a9edf5561de121952d
Parent 8d68b4a
Delta 1629 lines added, 1555 lines removed, 74-line increase