| () -> 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 ) ); |
| } | ||
| - 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(); |
| 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( |
| /** | ||
| + * The default apostrophe to use when exporting. | ||
| + */ | ||
| + public static final String APOS_DEFAULT = "apos"; | ||
| + | ||
| + /** | ||
| * Prevent instantiation. | ||
| */ |
| 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" | ||
| + ) | ||
| ); | ||
| } |
| 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 ) | ||
| + ); | ||
| } | ||
| +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; | ||
| + } | ||
| +} | ||
| 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 ); | ||
| } | ||
| * 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; | ||
| } | ||
| } |
| 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 ); |
| 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; | ||
| } | ||
| 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 \’ 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; | ||
| } | ||
| 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 ); | ||
| } | ||
| } | ||
| 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 ) ) | ||
| - ); | ||
| - } ); | ||
| } | ||
| 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 |
| 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(); | ||
| 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 |