| 11 | 11 | # Disable line ending normalize on checkin. |
| 12 | 12 | |
| 13 | *.bin binary | |
| 14 | *.exe binary | |
| 13 | 15 | *.gif binary |
| 14 | 16 | *.jar binary |
| 1 | #!/usr/bin/env bash | |
| 2 | ||
| 3 | # Writes the name for all OTF files found in the current directory or lower | |
| 4 | ||
| 5 | find src/main/resources/fonts -type f \( -name "*otf" -o -name "*ttf" \) -exec \ | |
| 6 | fc-scan --format "%{foundry}: %{family}\n" {} \; | uniq | sort | |
| 7 | ||
| 1 | 8 |
| 15 | 15 | ARG_JAVA_OS="linux" |
| 16 | 16 | ARG_JAVA_ARCH="amd64" |
| 17 | ARG_JAVA_VERSION="15.0.1" | |
| 18 | ARG_JAVA_UPDATE="9" | |
| 17 | ARG_JAVA_VERSION="15.0.2" | |
| 18 | ARG_JAVA_UPDATE="10" | |
| 19 | 19 | ARG_JAVA_DIR="java" |
| 20 | 20 |
| 1 | Copyright 2014-2019 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. | |
| 2 | ||
| 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. | |
| 4 | ||
| 5 | This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL | |
| 6 | ||
| 7 | ||
| 8 | ----------------------------------------------------------- | |
| 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | |
| 10 | ----------------------------------------------------------- | |
| 11 | ||
| 12 | PREAMBLE | |
| 13 | The goals of the Open Font License (OFL) are to stimulate worldwide | |
| 14 | development of collaborative font projects, to support the font creation | |
| 15 | efforts of academic and linguistic communities, and to provide a free and | |
| 16 | open framework in which fonts may be shared and improved in partnership | |
| 17 | with others. | |
| 18 | ||
| 19 | The OFL allows the licensed fonts to be used, studied, modified and | |
| 20 | redistributed freely as long as they are not sold by themselves. The | |
| 21 | fonts, including any derivative works, can be bundled, embedded, | |
| 22 | redistributed and/or sold with any software provided that any reserved | |
| 23 | names are not used by derivative works. The fonts and derivatives, | |
| 24 | however, cannot be released under any other type of license. The | |
| 25 | requirement for fonts to remain under this license does not apply | |
| 26 | to any document created using the fonts or their derivatives. | |
| 27 | ||
| 28 | DEFINITIONS | |
| 29 | "Font Software" refers to the set of files released by the Copyright | |
| 30 | Holder(s) under this license and clearly marked as such. This may | |
| 31 | include source files, build scripts and documentation. | |
| 32 | ||
| 33 | "Reserved Font Name" refers to any names specified as such after the | |
| 34 | copyright statement(s). | |
| 35 | ||
| 36 | "Original Version" refers to the collection of Font Software components as | |
| 37 | distributed by the Copyright Holder(s). | |
| 38 | ||
| 39 | "Modified Version" refers to any derivative made by adding to, deleting, | |
| 40 | or substituting -- in part or in whole -- any of the components of the | |
| 41 | Original Version, by changing formats or by porting the Font Software to a | |
| 42 | new environment. | |
| 43 | ||
| 44 | "Author" refers to any designer, engineer, programmer, technical | |
| 45 | writer or other person who contributed to the Font Software. | |
| 46 | ||
| 47 | PERMISSION & CONDITIONS | |
| 48 | Permission is hereby granted, free of charge, to any person obtaining | |
| 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, | |
| 50 | redistribute, and sell modified and unmodified copies of the Font | |
| 51 | Software, subject to the following conditions: | |
| 52 | ||
| 53 | 1) Neither the Font Software nor any of its individual components, | |
| 54 | in Original or Modified Versions, may be sold by itself. | |
| 55 | ||
| 56 | 2) Original or Modified Versions of the Font Software may be bundled, | |
| 57 | redistributed and/or sold with any software, provided that each copy | |
| 58 | contains the above copyright notice and this license. These can be | |
| 59 | included either as stand-alone text files, human-readable headers or | |
| 60 | in the appropriate machine-readable metadata fields within text or | |
| 61 | binary files as long as those fields can be easily viewed by the user. | |
| 62 | ||
| 63 | 3) No Modified Version of the Font Software may use the Reserved Font | |
| 64 | Name(s) unless explicit written permission is granted by the corresponding | |
| 65 | Copyright Holder. This restriction only applies to the primary font name as | |
| 66 | presented to the users. | |
| 67 | ||
| 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | |
| 69 | Software shall not be used to promote, endorse or advertise any | |
| 70 | Modified Version, except to acknowledge the contribution(s) of the | |
| 71 | Copyright Holder(s) and the Author(s) or with their explicit written | |
| 72 | permission. | |
| 73 | ||
| 74 | 5) The Font Software, modified or unmodified, in part or in whole, | |
| 75 | must be distributed entirely under this license, and must not be | |
| 76 | distributed under any other license. The requirement for fonts to | |
| 77 | remain under this license does not apply to any document created | |
| 78 | using the Font Software. | |
| 79 | ||
| 80 | TERMINATION | |
| 81 | This license becomes null and void if any of the above conditions are | |
| 82 | not met. | |
| 83 | ||
| 84 | DISCLAIMER | |
| 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | |
| 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | |
| 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | |
| 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | |
| 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | |
| 93 | OTHER DEALINGS IN THE FONT SOFTWARE. | |
| 1 | 94 |
| 1 | Copyright 2014-2019 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. | |
| 2 | ||
| 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. | |
| 4 | ||
| 5 | This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL | |
| 6 | ||
| 7 | ||
| 8 | ----------------------------------------------------------- | |
| 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | |
| 10 | ----------------------------------------------------------- | |
| 11 | ||
| 12 | PREAMBLE | |
| 13 | The goals of the Open Font License (OFL) are to stimulate worldwide | |
| 14 | development of collaborative font projects, to support the font creation | |
| 15 | efforts of academic and linguistic communities, and to provide a free and | |
| 16 | open framework in which fonts may be shared and improved in partnership | |
| 17 | with others. | |
| 18 | ||
| 19 | The OFL allows the licensed fonts to be used, studied, modified and | |
| 20 | redistributed freely as long as they are not sold by themselves. The | |
| 21 | fonts, including any derivative works, can be bundled, embedded, | |
| 22 | redistributed and/or sold with any software provided that any reserved | |
| 23 | names are not used by derivative works. The fonts and derivatives, | |
| 24 | however, cannot be released under any other type of license. The | |
| 25 | requirement for fonts to remain under this license does not apply | |
| 26 | to any document created using the fonts or their derivatives. | |
| 27 | ||
| 28 | DEFINITIONS | |
| 29 | "Font Software" refers to the set of files released by the Copyright | |
| 30 | Holder(s) under this license and clearly marked as such. This may | |
| 31 | include source files, build scripts and documentation. | |
| 32 | ||
| 33 | "Reserved Font Name" refers to any names specified as such after the | |
| 34 | copyright statement(s). | |
| 35 | ||
| 36 | "Original Version" refers to the collection of Font Software components as | |
| 37 | distributed by the Copyright Holder(s). | |
| 38 | ||
| 39 | "Modified Version" refers to any derivative made by adding to, deleting, | |
| 40 | or substituting -- in part or in whole -- any of the components of the | |
| 41 | Original Version, by changing formats or by porting the Font Software to a | |
| 42 | new environment. | |
| 43 | ||
| 44 | "Author" refers to any designer, engineer, programmer, technical | |
| 45 | writer or other person who contributed to the Font Software. | |
| 46 | ||
| 47 | PERMISSION & CONDITIONS | |
| 48 | Permission is hereby granted, free of charge, to any person obtaining | |
| 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, | |
| 50 | redistribute, and sell modified and unmodified copies of the Font | |
| 51 | Software, subject to the following conditions: | |
| 52 | ||
| 53 | 1) Neither the Font Software nor any of its individual components, | |
| 54 | in Original or Modified Versions, may be sold by itself. | |
| 55 | ||
| 56 | 2) Original or Modified Versions of the Font Software may be bundled, | |
| 57 | redistributed and/or sold with any software, provided that each copy | |
| 58 | contains the above copyright notice and this license. These can be | |
| 59 | included either as stand-alone text files, human-readable headers or | |
| 60 | in the appropriate machine-readable metadata fields within text or | |
| 61 | binary files as long as those fields can be easily viewed by the user. | |
| 62 | ||
| 63 | 3) No Modified Version of the Font Software may use the Reserved Font | |
| 64 | Name(s) unless explicit written permission is granted by the corresponding | |
| 65 | Copyright Holder. This restriction only applies to the primary font name as | |
| 66 | presented to the users. | |
| 67 | ||
| 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | |
| 69 | Software shall not be used to promote, endorse or advertise any | |
| 70 | Modified Version, except to acknowledge the contribution(s) of the | |
| 71 | Copyright Holder(s) and the Author(s) or with their explicit written | |
| 72 | permission. | |
| 73 | ||
| 74 | 5) The Font Software, modified or unmodified, in part or in whole, | |
| 75 | must be distributed entirely under this license, and must not be | |
| 76 | distributed under any other license. The requirement for fonts to | |
| 77 | remain under this license does not apply to any document created | |
| 78 | using the Font Software. | |
| 79 | ||
| 80 | TERMINATION | |
| 81 | This license becomes null and void if any of the above conditions are | |
| 82 | not met. | |
| 83 | ||
| 84 | DISCLAIMER | |
| 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | |
| 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | |
| 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | |
| 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
| 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | |
| 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | |
| 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | |
| 93 | OTHER DEALINGS IN THE FONT SOFTWARE. | |
| 94 | 1 |
| 180 | 180 | * Default preview font name. |
| 181 | 181 | */ |
| 182 | public static final String FONT_NAME_PREVIEW_DEFAULT = "Source Serif Pro"; | |
| 182 | public static final String FONT_NAME_PREVIEW_DEFAULT = "Source Serif 4"; | |
| 183 | 183 | |
| 184 | 184 | /** |
| ... | ||
| 222 | 222 | */ |
| 223 | 223 | public static final String ICON_SIZE_DEFAULT = "1.2em"; |
| 224 | ||
| 225 | public static final String DIAGRAM_SERVER_NAME = "kroki.io"; | |
| 224 | 226 | |
| 225 | 227 | /** |
| 388 | 388 | final var resource = tab.getContent(); |
| 389 | 389 | |
| 390 | if( !(resource instanceof TextResource) ) { | |
| 390 | // The definition panes auto-save, so being specific here prevents | |
| 391 | // closing the definitions in the situation where the user wants to | |
| 392 | // continue editing (i.e., possibly save unsaved work). | |
| 393 | if( !(resource instanceof TextEditor) ) { | |
| 391 | 394 | continue; |
| 392 | 395 | } |
| 393 | 396 | |
| 394 | if( canClose( (TextResource) resource ) ) { | |
| 397 | if( canClose( (TextEditor) resource ) ) { | |
| 395 | 398 | tabIterator.remove(); |
| 396 | 399 | close( tab ); |
| 102 | 102 | |
| 103 | 103 | internal.addListener( |
| 104 | ( c, o, n ) -> applyStylesheets( scene, inTheme, exTheme ) | |
| 104 | ( c, o, n ) -> { | |
| 105 | if( n != null ) { | |
| 106 | applyStylesheets( scene, n, exTheme ); | |
| 107 | } | |
| 108 | } | |
| 105 | 109 | ); |
| 106 | 110 | |
| ... | ||
| 137 | 141 | * @param scene The scene to stylize. |
| 138 | 142 | * @param internal The CSS file name bundled with the application. |
| 143 | * @param external The (optional) customized CSS file specified by the user. | |
| 139 | 144 | */ |
| 140 | 145 | private void applyStylesheets( |
| ... | ||
| 147 | 152 | |
| 148 | 153 | try { |
| 149 | if( external.canRead() && !external.isDirectory() ) { | |
| 154 | if( external != null && external.canRead() && !external.isDirectory() ) { | |
| 150 | 155 | stylesheets.add( external.toURI().toURL().toString() ); |
| 151 | ||
| 152 | 156 | mFileWatchService.register( external ); |
| 153 | 157 | } |
| 34 | 34 | @SuppressWarnings( "SameParameterValue" ) |
| 35 | 35 | private static String resolve( final ResourceBundle props, final String s ) { |
| 36 | final int len = s.length(); | |
| 37 | final Stack<StringBuilder> stack = new Stack<>(); | |
| 38 | ||
| 39 | StringBuilder sb = new StringBuilder( 256 ); | |
| 40 | boolean open = false; | |
| 36 | final var len = s.length(); | |
| 37 | final var stack = new Stack<StringBuilder>(); | |
| 38 | var sb = new StringBuilder( 256 ); | |
| 39 | var open = false; | |
| 41 | 40 | |
| 42 | for( int i = 0; i < len; i++ ) { | |
| 43 | final char c = s.charAt( i ); | |
| 41 | for( var i = 0; i < len; i++ ) { | |
| 42 | final var c = s.charAt( i ); | |
| 44 | 43 | |
| 45 | 44 | switch( c ) { |
| ... | ||
| 64 | 63 | if( open ) { |
| 65 | 64 | open = false; |
| 66 | final String name = sb.toString(); | |
| 65 | final var name = sb.toString(); | |
| 67 | 66 | |
| 68 | 67 | sb = stack.pop(); |
| ... | ||
| 96 | 95 | try { |
| 97 | 96 | return resolve( RESOURCE_BUNDLE, RESOURCE_BUNDLE.getString( key ) ); |
| 98 | } catch( final Exception ex ) { | |
| 97 | } catch( final Exception ignored ) { | |
| 99 | 98 | return key; |
| 100 | 99 | } |
| 16 | 16 | */ |
| 17 | 17 | public class StatusEvent implements AppEvent { |
| 18 | private static final String PACKAGE_NAME = MainApp.class.getPackageName(); | |
| 19 | ||
| 18 | 20 | /** |
| 19 | 21 | * Indicates that there are no issues to bring to the user's attention. |
| ... | ||
| 80 | 82 | private static boolean filter( final StackTraceElement e ) { |
| 81 | 83 | final var clazz = e.getClassName(); |
| 82 | return clazz.contains( MainApp.class.getPackageName() ) || | |
| 83 | clazz.startsWith( "org.renjin" ); | |
| 84 | return clazz.contains( PACKAGE_NAME ) || | |
| 85 | clazz.contains( "org.renjin." ) || | |
| 86 | clazz.contains( "sun." ) || | |
| 87 | clazz.contains( "java." ); | |
| 84 | 88 | } |
| 85 | 89 | |
| 86 | 90 | /** |
| 87 | 91 | * Returns the message used to construct the event. |
| 88 | 92 | * |
| 89 | 93 | * @return The message for this event. |
| 90 | 94 | */ |
| 91 | public String toString() { | |
| 95 | public String getMessage() { | |
| 92 | 96 | return mMessage; |
| 93 | 97 | } |
| ... | ||
| 126 | 130 | */ |
| 127 | 131 | public static void clue( final Throwable problem ) { |
| 128 | fireStatusEvent( problem.getMessage() ); | |
| 132 | fireStatusEvent( problem.getMessage(), problem ); | |
| 129 | 133 | } |
| 130 | 134 | |
| 131 | 135 | private static void fireStatusEvent( final String message ) { |
| 132 | 136 | new StatusEvent( message ).fire(); |
| 133 | 137 | } |
| 134 | 138 | |
| 135 | private static void fireStatusEvent( final String message, final Throwable problem ) { | |
| 139 | private static void fireStatusEvent( | |
| 140 | final String message, final Throwable problem ) { | |
| 136 | 141 | new StatusEvent( message, problem ).fire(); |
| 137 | 142 | } |
| 7 | 7 | import java.io.File; |
| 8 | 8 | import java.io.IOException; |
| 9 | import java.nio.file.FileSystems; | |
| 10 | 9 | import java.nio.file.Path; |
| 11 | 10 | import java.nio.file.WatchKey; |
| 12 | 11 | import java.nio.file.WatchService; |
| 13 | 12 | import java.util.Set; |
| 14 | 13 | import java.util.concurrent.ConcurrentHashMap; |
| 15 | 14 | |
| 15 | import static java.nio.file.FileSystems.getDefault; | |
| 16 | 16 | import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; |
| 17 | 17 | import static java.util.Collections.newSetFromMap; |
| ... | ||
| 40 | 40 | */ |
| 41 | 41 | public FileWatchService( final File... files ) { |
| 42 | WatchService watchService; | |
| 42 | mWatchService = createWatchService(); | |
| 43 | 43 | |
| 44 | 44 | try { |
| 45 | watchService = FileSystems.getDefault().newWatchService(); | |
| 46 | ||
| 47 | 45 | for( final var file : files ) { |
| 48 | 46 | register( file ); |
| 49 | 47 | } |
| 50 | } catch( final Exception ignored ) { | |
| 51 | // Create a fallback that allows the class to be instantiated and used | |
| 52 | // without without preventing the application from launching. | |
| 53 | watchService = new PollingWatchService(); | |
| 48 | } catch( final Exception ex ) { | |
| 49 | throw new RuntimeException( ex ); | |
| 54 | 50 | } |
| 55 | ||
| 56 | mWatchService = watchService; | |
| 57 | 51 | } |
| 58 | 52 | |
| ... | ||
| 216 | 210 | |
| 217 | 211 | return directory.toPath(); |
| 212 | } | |
| 213 | ||
| 214 | private WatchService createWatchService() { | |
| 215 | try { | |
| 216 | return getDefault().newWatchService(); | |
| 217 | } catch( final Exception ex ) { | |
| 218 | // Create a fallback that allows the class to be instantiated and used | |
| 219 | // without without preventing the application from launching. | |
| 220 | return new PollingWatchService(); | |
| 221 | } | |
| 218 | 222 | } |
| 219 | 223 | } |
| 2 | 2 | package com.keenwrite.io; |
| 3 | 3 | |
| 4 | import javax.net.ssl.*; | |
| 4 | 5 | import java.net.MalformedURLException; |
| 6 | import java.net.Socket; | |
| 5 | 7 | import java.net.URI; |
| 6 | 8 | import java.net.URL; |
| 7 | 9 | import java.net.http.HttpClient; |
| 8 | 10 | import java.net.http.HttpRequest; |
| 11 | import java.security.cert.X509Certificate; | |
| 9 | 12 | |
| 10 | 13 | import static com.keenwrite.events.StatusEvent.clue; |
| 11 | 14 | import static com.keenwrite.io.MediaType.UNDEFINED; |
| 12 | 15 | import static java.net.http.HttpClient.Redirect.NORMAL; |
| 13 | 16 | import static java.net.http.HttpRequest.BodyPublishers.noBody; |
| 14 | import static java.net.http.HttpResponse.BodyHandlers.discarding; | |
| 17 | import static java.net.http.HttpResponse.BodyHandlers.ofString; | |
| 15 | 18 | import static java.time.Duration.ofSeconds; |
| 16 | 19 | |
| 17 | 20 | /** |
| 18 | 21 | * Responsible for determining {@link MediaType} based on the content-type from |
| 19 | 22 | * an HTTP request. |
| 20 | 23 | */ |
| 21 | 24 | public final class HttpMediaType { |
| 22 | 25 | |
| 23 | private final static HttpClient HTTP_CLIENT = HttpClient | |
| 26 | static { | |
| 27 | disableSSLVerification(); | |
| 28 | } | |
| 29 | ||
| 30 | private static final HttpClient HTTP_CLIENT = HttpClient | |
| 24 | 31 | .newBuilder() |
| 25 | .connectTimeout( ofSeconds( 5 ) ) | |
| 32 | .connectTimeout( ofSeconds( 10 ) ) | |
| 26 | 33 | .followRedirects( NORMAL ) |
| 27 | 34 | .build(); |
| ... | ||
| 35 | 42 | * unmapped. |
| 36 | 43 | * @throws MalformedURLException The {@link URI} could not be converted to |
| 37 | * a {@link URL}. | |
| 44 | * an instance of {@link URL}. | |
| 38 | 45 | */ |
| 39 | 46 | public static MediaType valueFrom( final URI uri ) |
| 40 | 47 | throws MalformedURLException { |
| 41 | 48 | final var mediaType = new MediaType[]{UNDEFINED}; |
| 42 | 49 | |
| 43 | 50 | try { |
| 44 | 51 | clue( "Main.status.image.request.init" ); |
| 45 | 52 | final var request = HttpRequest |
| 46 | .newBuilder( uri ) | |
| 53 | .newBuilder() | |
| 47 | 54 | .method( "HEAD", noBody() ) |
| 55 | .uri( uri ) | |
| 48 | 56 | .build(); |
| 49 | 57 | clue( "Main.status.image.request.fetch", uri.getHost() ); |
| 50 | final var response = HTTP_CLIENT.send( request, discarding() ); | |
| 58 | final var response = HTTP_CLIENT.send( request, ofString() ); | |
| 51 | 59 | final var headers = response.headers(); |
| 52 | 60 | final var map = headers.map(); |
| ... | ||
| 69 | 77 | } |
| 70 | 78 | } ); |
| 79 | ||
| 80 | clue(); | |
| 71 | 81 | } catch( final Exception ex ) { |
| 72 | 82 | clue( ex ); |
| 73 | 83 | } |
| 74 | 84 | |
| 75 | 85 | return mediaType[ 0 ]; |
| 86 | } | |
| 87 | ||
| 88 | // Method used for bypassing SSL verification | |
| 89 | private static void disableSSLVerification() { | |
| 90 | ||
| 91 | TrustManager[] trustAllCerts = | |
| 92 | new TrustManager[]{new X509ExtendedTrustManager() { | |
| 93 | @Override | |
| 94 | public void checkClientTrusted( X509Certificate[] chain, | |
| 95 | String authType, | |
| 96 | Socket socket ) { | |
| 97 | ||
| 98 | } | |
| 99 | ||
| 100 | @Override | |
| 101 | public void checkServerTrusted( X509Certificate[] chain, | |
| 102 | String authType, | |
| 103 | Socket socket ) { | |
| 104 | ||
| 105 | } | |
| 106 | ||
| 107 | @Override | |
| 108 | public void checkClientTrusted( X509Certificate[] chain, | |
| 109 | String authType, | |
| 110 | SSLEngine engine ) { | |
| 111 | ||
| 112 | } | |
| 113 | ||
| 114 | @Override | |
| 115 | public void checkServerTrusted( X509Certificate[] chain, | |
| 116 | String authType, | |
| 117 | SSLEngine engine ) { | |
| 118 | ||
| 119 | } | |
| 120 | ||
| 121 | @Override | |
| 122 | public java.security.cert.X509Certificate[] getAcceptedIssuers() { | |
| 123 | return null; | |
| 124 | } | |
| 125 | ||
| 126 | @Override | |
| 127 | public void checkClientTrusted( | |
| 128 | X509Certificate[] certs, String authType ) { | |
| 129 | } | |
| 130 | ||
| 131 | @Override | |
| 132 | public void checkServerTrusted( | |
| 133 | X509Certificate[] certs, String authType ) { | |
| 134 | } | |
| 135 | }}; | |
| 136 | ||
| 137 | try { | |
| 138 | final var context = SSLContext.getInstance( "SSL" ); | |
| 139 | context.init( null, trustAllCerts, new java.security.SecureRandom() ); | |
| 140 | HttpsURLConnection.setDefaultSSLSocketFactory( context.getSocketFactory() ); | |
| 141 | HttpsURLConnection.setDefaultHostnameVerifier( ( hostname, session ) -> true ); | |
| 142 | } catch( final Exception ex ) { | |
| 143 | clue( ex ); | |
| 144 | } | |
| 76 | 145 | } |
| 77 | 146 | } |
| 418 | 418 | private Object unmarshall( |
| 419 | 419 | final Property<?> property, final Object configValue ) { |
| 420 | String setting = configValue.toString(); | |
| 421 | ||
| 422 | // TODO: #118 - Font upgrade, which can be removed in a few releases. | |
| 423 | if( setting.equalsIgnoreCase( "Source Serif Pro" ) ) { | |
| 424 | setting = "Source Serif 4"; | |
| 425 | } | |
| 426 | ||
| 420 | 427 | return UNMARSHALL |
| 421 | 428 | .getOrDefault( property.getClass(), ( value ) -> value ) |
| 422 | .apply( configValue.toString() ); | |
| 429 | .apply( setting ); | |
| 423 | 430 | } |
| 424 | 431 | |
| 425 | 432 | private Object marshall( final Property<?> property ) { |
| 426 | return MARSHALL | |
| 433 | return property.getValue() == null | |
| 434 | ? null | |
| 435 | : MARSHALL | |
| 427 | 436 | .getOrDefault( property.getClass(), ( __ ) -> property.getValue() ) |
| 428 | 437 | .apply( property.getValue().toString() ); |
| 1 | /* Copyright 2006 Patrick Wright | |
| 2 | * Copyright 2007 Wisconsin Court System | |
| 3 | * Copyright 2020-2021 White Magic Software, Ltd. | |
| 4 | * | |
| 5 | * This program is free software; you can redistribute it and/or | |
| 6 | * modify it under the terms of the GNU Lesser General Public License | |
| 7 | * as published by the Free Software Foundation; either version 2.1 | |
| 8 | * of the License, or (at your option) any later version. | |
| 9 | * | |
| 10 | * This program is distributed in the hope that it will be useful, | |
| 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 13 | * GNU Lesser General Public License for more details. | |
| 14 | * | |
| 15 | * You should have received a copy of the GNU Lesser General Public License | |
| 16 | * along with this program; if not, write to the Free Software | |
| 17 | * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. | |
| 18 | */ | |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 19 | 2 | package com.keenwrite.preview; |
| 20 | 3 |
| 23 | 23 | import static java.awt.Desktop.Action.BROWSE; |
| 24 | 24 | import static java.awt.Desktop.getDesktop; |
| 25 | import static java.lang.Boolean.FALSE; | |
| 26 | import static java.lang.Boolean.TRUE; | |
| 25 | 27 | import static javax.swing.SwingUtilities.invokeLater; |
| 26 | 28 | import static javax.swing.SwingUtilities.isEventDispatchThread; |
| ... | ||
| 40 | 42 | @Override |
| 41 | 43 | public void documentStarted() { |
| 42 | mReadyProperty.setValue( Boolean.FALSE ); | |
| 44 | mReadyProperty.setValue( FALSE ); | |
| 43 | 45 | } |
| 44 | 46 | |
| 45 | 47 | @Override |
| 46 | 48 | public void documentLoaded() { |
| 47 | mReadyProperty.setValue( Boolean.TRUE ); | |
| 49 | mReadyProperty.setValue( TRUE ); | |
| 48 | 50 | } |
| 49 | 51 | } |
| 2 | 2 | package com.keenwrite.preview; |
| 3 | 3 | |
| 4 | import com.keenwrite.preferences.LocaleProperty; | |
| 5 | import com.keenwrite.preferences.Workspace; | |
| 6 | import javafx.application.Platform; | |
| 7 | import javafx.beans.property.DoubleProperty; | |
| 8 | import javafx.beans.property.StringProperty; | |
| 9 | import javafx.embed.swing.SwingNode; | |
| 10 | import org.xhtmlrenderer.render.Box; | |
| 11 | import org.xhtmlrenderer.swing.SwingReplacedElementFactory; | |
| 12 | ||
| 13 | import javax.swing.*; | |
| 14 | import java.awt.*; | |
| 15 | import java.net.URL; | |
| 16 | import java.nio.file.Path; | |
| 17 | import java.util.Locale; | |
| 18 | ||
| 19 | import static com.keenwrite.Constants.*; | |
| 20 | import static com.keenwrite.Messages.get; | |
| 21 | import static com.keenwrite.events.StatusEvent.clue; | |
| 22 | import static com.keenwrite.preferences.WorkspaceKeys.*; | |
| 23 | import static java.lang.Math.max; | |
| 24 | import static java.lang.String.format; | |
| 25 | import static java.lang.Thread.sleep; | |
| 26 | import static javafx.application.Platform.runLater; | |
| 27 | import static javafx.scene.CacheHint.SPEED; | |
| 28 | import static javax.swing.SwingUtilities.invokeLater; | |
| 29 | ||
| 30 | /** | |
| 31 | * Responsible for parsing an HTML document. | |
| 32 | */ | |
| 33 | public final class HtmlPreview extends SwingNode { | |
| 34 | ||
| 35 | // The order is important: Swing factory will replace SVG images with | |
| 36 | // a blank image, which will cause the chained factory to cache the image | |
| 37 | // and exit. Instead, the SVG must execute first to rasterize the content. | |
| 38 | // Consequently, the chained factory must maintain insertion order. | |
| 39 | private static final ChainedReplacedElementFactory FACTORY | |
| 40 | = new ChainedReplacedElementFactory( | |
| 41 | new SvgReplacedElementFactory(), | |
| 42 | new SwingReplacedElementFactory() | |
| 43 | ); | |
| 44 | ||
| 45 | /** | |
| 46 | * Render CSS using points (pt) not pixels (px) to reduce the chance of | |
| 47 | * poor rendering. The {@link #head()} method fills out the placeholders. | |
| 48 | */ | |
| 49 | private static final String HTML_HEAD = | |
| 50 | """ | |
| 51 | <!DOCTYPE html> | |
| 52 | <html lang='%s'><head><title> </title><meta charset='utf-8'> | |
| 53 | <link rel='stylesheet' href='%s'> | |
| 54 | <link rel='stylesheet' href='%s'> | |
| 55 | <style>body{font-family:'%s';font-size: %spt;}</style> | |
| 56 | <base href='%s'> | |
| 57 | </head><body> | |
| 58 | """; | |
| 59 | ||
| 60 | private static final String HTML_TAIL = "</body></html>"; | |
| 61 | ||
| 62 | private static final URL HTML_STYLE_PREVIEW = toUrl( STYLESHEET_PREVIEW ); | |
| 63 | ||
| 64 | /** | |
| 65 | * The buffer is reused so that previous memory allocations need not repeat. | |
| 66 | */ | |
| 67 | private final StringBuilder mHtmlDocument = new StringBuilder( 65536 ); | |
| 68 | ||
| 69 | private HtmlPanel mView; | |
| 70 | private JScrollPane mScrollPane; | |
| 71 | private String mBaseUriPath = ""; | |
| 72 | private URL mLocaleUrl; | |
| 73 | private final Workspace mWorkspace; | |
| 74 | ||
| 75 | /** | |
| 76 | * Creates a new preview pane that can scroll to the caret position within the | |
| 77 | * document. | |
| 78 | * | |
| 79 | * @param workspace Contains locale and font size information. | |
| 80 | */ | |
| 81 | public HtmlPreview( final Workspace workspace ) { | |
| 82 | mWorkspace = workspace; | |
| 83 | mLocaleUrl = toUrl( getLocale() ); | |
| 84 | ||
| 85 | // Attempts to prevent a flash of black un-styled content upon load. | |
| 86 | setStyle( "-fx-background-color: white;" ); | |
| 87 | ||
| 88 | invokeLater( () -> { | |
| 89 | mView = new HtmlPanel(); | |
| 90 | mScrollPane = new JScrollPane( mView ); | |
| 91 | ||
| 92 | // Enabling the cache attempts to prevent black flashes when resizing. | |
| 93 | setCache( true ); | |
| 94 | setCacheHint( SPEED ); | |
| 95 | setContent( mScrollPane ); | |
| 96 | ||
| 97 | final var context = mView.getSharedContext(); | |
| 98 | final var textRenderer = context.getTextRenderer(); | |
| 99 | context.setReplacedElementFactory( FACTORY ); | |
| 100 | textRenderer.setSmoothingThreshold( 0 ); | |
| 101 | ||
| 102 | localeProperty().addListener( ( c, o, n ) -> { | |
| 103 | mLocaleUrl = toUrl( getLocale() ); | |
| 104 | rerender(); | |
| 105 | } ); | |
| 106 | ||
| 107 | fontNameProperty().addListener( ( c, o, n ) -> rerender() ); | |
| 108 | fontSizeProperty().addListener( ( c, o, n ) -> rerender() ); | |
| 109 | } ); | |
| 110 | } | |
| 111 | ||
| 112 | /** | |
| 113 | * Updates the internal HTML source shown in the preview pane. | |
| 114 | * | |
| 115 | * @param html The new HTML document to display. | |
| 116 | */ | |
| 117 | public void render( final String html ) { | |
| 118 | mView.render( decorate( html ), getBaseUri() ); | |
| 119 | } | |
| 120 | ||
| 121 | /** | |
| 122 | * Clears the caches then rerenders the content. | |
| 123 | */ | |
| 124 | public void refresh() { | |
| 125 | FACTORY.clearCache(); | |
| 126 | rerender(); | |
| 127 | } | |
| 128 | ||
| 129 | private void rerender() { | |
| 130 | render( mHtmlDocument.toString() ); | |
| 131 | } | |
| 132 | ||
| 133 | /** | |
| 134 | * Attaches the HTML head prefix and HTML tail suffix to the given HTML | |
| 135 | * string. | |
| 136 | * | |
| 137 | * @param html The HTML to adorn with opening and closing tags. | |
| 138 | * @return A complete HTML document, ready for rendering. | |
| 139 | */ | |
| 140 | private String decorate( final String html ) { | |
| 141 | mHtmlDocument.setLength( 0 ); | |
| 142 | mHtmlDocument.append( html ); | |
| 143 | return head() + mHtmlDocument + tail(); | |
| 144 | } | |
| 145 | ||
| 146 | private String head() { | |
| 147 | return format( | |
| 148 | HTML_HEAD, | |
| 149 | getLocale().getLanguage(), | |
| 150 | HTML_STYLE_PREVIEW, | |
| 151 | mLocaleUrl, | |
| 152 | getFontName(), | |
| 153 | getFontSize(), | |
| 154 | mBaseUriPath | |
| 155 | ); | |
| 156 | } | |
| 157 | ||
| 158 | private String tail() { | |
| 159 | return HTML_TAIL; | |
| 160 | } | |
| 161 | ||
| 162 | /** | |
| 163 | * Clears the preview pane by rendering an empty string. | |
| 164 | */ | |
| 165 | public void clear() { | |
| 166 | render( "" ); | |
| 167 | } | |
| 168 | ||
| 169 | /** | |
| 170 | * Sets the base URI to the containing directory the file being edited. | |
| 171 | * | |
| 172 | * @param path The path to the file being edited. | |
| 173 | */ | |
| 174 | public void setBaseUri( final Path path ) { | |
| 175 | final var parent = path.getParent(); | |
| 176 | mBaseUriPath = parent == null ? "" : parent.toUri().toString(); | |
| 177 | } | |
| 178 | ||
| 179 | /** | |
| 180 | * Scrolls to the closest element matching the given identifier without | |
| 181 | * waiting for the document to be ready. | |
| 182 | * | |
| 183 | * @param id Scroll the preview pane to this unique paragraph identifier. | |
| 184 | */ | |
| 185 | public void scrollTo( final String id ) { | |
| 186 | final Runnable scrollToBox = () -> { | |
| 187 | int iter = 0; | |
| 188 | Box box = null; | |
| 189 | ||
| 190 | while( iter++ < 3 && ((box = mView.getBoxById( id )) == null) ) { | |
| 191 | try { | |
| 192 | sleep( 10 ); | |
| 193 | } catch( final Exception ex ) { | |
| 194 | clue( ex ); | |
| 195 | } | |
| 196 | } | |
| 197 | ||
| 198 | scrollTo( box ); | |
| 199 | }; | |
| 200 | ||
| 201 | if( Platform.isFxApplicationThread() ) { | |
| 202 | scrollToBox.run(); | |
| 203 | } | |
| 204 | else { | |
| 205 | runLater( scrollToBox ); | |
| 206 | } | |
| 207 | } | |
| 208 | ||
| 209 | /** | |
| 210 | * Scrolls to the location specified by the {@link Box} that corresponds | |
| 211 | * to a point somewhere in the preview pane. If there is no caret, then | |
| 212 | * this will not change the scroll position. Changing the scroll position | |
| 213 | * to the top if the {@link Box} instance is {@code null} will result in | |
| 214 | * jumping around a lot and inconsistent synchronization issues. | |
| 215 | * | |
| 216 | * @param box The rectangular region containing the caret, or {@code null} | |
| 217 | * if the HTML does not have a caret. | |
| 218 | */ | |
| 219 | private void scrollTo( final Box box ) { | |
| 220 | if( box != null ) { | |
| 221 | scrollTo( createPoint( box ) ); | |
| 222 | } | |
| 223 | } | |
| 224 | ||
| 225 | private void scrollTo( final Point point ) { | |
| 226 | invokeLater( () -> { | |
| 227 | mView.scrollTo( point ); | |
| 228 | getScrollPane().repaint(); | |
| 229 | } ); | |
| 230 | } | |
| 231 | ||
| 232 | /** | |
| 233 | * Creates a {@link Point} to use as a reference for scrolling to the area | |
| 234 | * described by the given {@link Box}. The {@link Box} coordinates are used | |
| 235 | * to populate the {@link Point}'s location, with minor adjustments for | |
| 236 | * vertical centering. | |
| 237 | * | |
| 238 | * @param box The {@link Box} that represents a scrolling anchor reference. | |
| 239 | * @return A coordinate suitable for scrolling to. | |
| 240 | */ | |
| 241 | private Point createPoint( final Box box ) { | |
| 242 | assert box != null; | |
| 243 | ||
| 244 | // Scroll back up by half the height of the scroll bar to keep the typing | |
| 245 | // area within the view port. Otherwise the view port will have jumped too | |
| 246 | // high up and the most recently typed letters won't be visible. | |
| 247 | int y = max( box.getAbsY() - getVerticalScrollBarHeight() / 2, 0 ); | |
| 248 | int x = box.getAbsX(); | |
| 249 | ||
| 250 | if( !box.getStyle().isInline() ) { | |
| 251 | final var margin = box.getMargin( mView.getLayoutContext() ); | |
| 252 | y += margin.top(); | |
| 253 | x += margin.left(); | |
| 254 | } | |
| 255 | ||
| 256 | return new Point( x, y ); | |
| 257 | } | |
| 258 | ||
| 259 | private String getBaseUri() { | |
| 260 | return mBaseUriPath; | |
| 261 | } | |
| 262 | ||
| 263 | private JScrollPane getScrollPane() { | |
| 264 | return mScrollPane; | |
| 265 | } | |
| 266 | ||
| 267 | public JScrollBar getVerticalScrollBar() { | |
| 268 | return getScrollPane().getVerticalScrollBar(); | |
| 269 | } | |
| 270 | ||
| 271 | private int getVerticalScrollBarHeight() { | |
| 272 | return getVerticalScrollBar().getHeight(); | |
| 273 | } | |
| 274 | ||
| 275 | /** | |
| 276 | * Returns the ISO 639 alpha-2 or alpha-3 language code followed by a hyphen | |
| 277 | * followed by the ISO 15924 alpha-4 script code, followed by an ISO 3166 | |
| 278 | * alpha-2 country code or UN M.49 numeric-3 area code. For example, this | |
| 279 | * could return "en-Latn-CA" for Canadian English written in the Latin | |
| 280 | * character set. | |
| 281 | * | |
| 282 | * @return Unique identifier for language and country. | |
| 283 | */ | |
| 284 | private static URL toUrl( final Locale locale ) { | |
| 285 | return toUrl( | |
| 286 | get( | |
| 287 | sSettings.getSetting( STYLESHEET_PREVIEW_LOCALE, "" ), | |
| 288 | locale.getLanguage(), | |
| 289 | locale.getScript(), | |
| 290 | locale.getCountry() | |
| 291 | ) | |
| 292 | ); | |
| 293 | } | |
| 294 | ||
| 295 | private static URL toUrl( final String path ) { | |
| 296 | return HtmlPreview.class.getResource( path ); | |
| 297 | } | |
| 298 | ||
| 299 | private Locale getLocale() { | |
| 300 | return localeProperty().toLocale(); | |
| 301 | } | |
| 302 | ||
| 303 | private LocaleProperty localeProperty() { | |
| 304 | return mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE ); | |
| 305 | } | |
| 306 | ||
| 307 | private String getFontName() { | |
| 308 | return fontNameProperty().get(); | |
| 309 | } | |
| 310 | ||
| 311 | private StringProperty fontNameProperty() { | |
| 312 | return mWorkspace.stringProperty( KEY_UI_FONT_PREVIEW_NAME ); | |
| 313 | } | |
| 314 | ||
| 315 | private double getFontSize() { | |
| 316 | return fontSizeProperty().get(); | |
| 317 | } | |
| 318 | ||
| 4 | import com.keenwrite.Constants; | |
| 5 | import com.keenwrite.preferences.LocaleProperty; | |
| 6 | import com.keenwrite.preferences.Workspace; | |
| 7 | import javafx.application.Platform; | |
| 8 | import javafx.beans.property.DoubleProperty; | |
| 9 | import javafx.beans.property.StringProperty; | |
| 10 | import javafx.embed.swing.SwingNode; | |
| 11 | import org.xhtmlrenderer.render.Box; | |
| 12 | import org.xhtmlrenderer.swing.SwingReplacedElementFactory; | |
| 13 | ||
| 14 | import javax.swing.*; | |
| 15 | import java.awt.*; | |
| 16 | import java.net.URL; | |
| 17 | import java.nio.file.Path; | |
| 18 | import java.util.Locale; | |
| 19 | ||
| 20 | import static com.keenwrite.Constants.*; | |
| 21 | import static com.keenwrite.Messages.get; | |
| 22 | import static com.keenwrite.events.StatusEvent.clue; | |
| 23 | import static com.keenwrite.preferences.WorkspaceKeys.*; | |
| 24 | import static java.lang.Math.max; | |
| 25 | import static java.lang.String.format; | |
| 26 | import static java.lang.Thread.sleep; | |
| 27 | import static javafx.application.Platform.runLater; | |
| 28 | import static javafx.scene.CacheHint.SPEED; | |
| 29 | import static javax.swing.SwingUtilities.invokeLater; | |
| 30 | ||
| 31 | /** | |
| 32 | * Responsible for parsing an HTML document. | |
| 33 | */ | |
| 34 | public final class HtmlPreview extends SwingNode { | |
| 35 | ||
| 36 | /** | |
| 37 | * The order is important: Swing factory will replace SVG images with | |
| 38 | * a blank image, which will cause the chained factory to cache the image | |
| 39 | * and exit. Instead, the SVG must execute first to rasterize the content. | |
| 40 | * Consequently, the chained factory must maintain insertion order. | |
| 41 | */ | |
| 42 | private static final ChainedReplacedElementFactory FACTORY | |
| 43 | = new ChainedReplacedElementFactory( | |
| 44 | new SvgReplacedElementFactory(), | |
| 45 | new SwingReplacedElementFactory() | |
| 46 | ); | |
| 47 | ||
| 48 | /** | |
| 49 | * Used to populate the {@link #HTML_HEAD} with stylesheet file references. | |
| 50 | */ | |
| 51 | private static final String HTML_STYLESHEET = | |
| 52 | "<link rel='stylesheet' href='%s'/>"; | |
| 53 | ||
| 54 | /** | |
| 55 | * Render CSS using points (pt) not pixels (px) to reduce the chance of | |
| 56 | * poor rendering. The {@link #head()} method fills out the placeholders. | |
| 57 | * When the user has not set a locale, only one stylesheet is added to | |
| 58 | * the document. | |
| 59 | * <p> | |
| 60 | * Do not use points, only pixels here. | |
| 61 | * </p> | |
| 62 | */ | |
| 63 | private static final String HTML_HEAD = | |
| 64 | """ | |
| 65 | <!doctype html> | |
| 66 | <html lang='%s'><head><title> </title><meta charset='utf-8'/> | |
| 67 | %s%s<style>body{font-family:'%s';font-size: %dpx;}</style> | |
| 68 | <base href='%s'/></head><body> | |
| 69 | """; | |
| 70 | ||
| 71 | private static final String HTML_TAIL = "</body></html>"; | |
| 72 | ||
| 73 | private static final URL HTML_STYLE_PREVIEW = toUrl( STYLESHEET_PREVIEW ); | |
| 74 | ||
| 75 | /** | |
| 76 | * The buffer is reused so that previous memory allocations need not repeat. | |
| 77 | */ | |
| 78 | private final StringBuilder mHtmlDocument = new StringBuilder( 65536 ); | |
| 79 | ||
| 80 | private HtmlPanel mView; | |
| 81 | private JScrollPane mScrollPane; | |
| 82 | private String mBaseUriPath = ""; | |
| 83 | ||
| 84 | /** | |
| 85 | * Populates {@link Constants#STYLESHEET_PREVIEW_LOCALE} for stylesheet. | |
| 86 | */ | |
| 87 | private URL mLocaleUrl; | |
| 88 | ||
| 89 | private final Workspace mWorkspace; | |
| 90 | ||
| 91 | /** | |
| 92 | * Creates a new preview pane that can scroll to the caret position within the | |
| 93 | * document. | |
| 94 | * | |
| 95 | * @param workspace Contains locale and font size information. | |
| 96 | */ | |
| 97 | public HtmlPreview( final Workspace workspace ) { | |
| 98 | mWorkspace = workspace; | |
| 99 | mLocaleUrl = toUrl( getLocale() ); | |
| 100 | ||
| 101 | // Attempts to prevent a flash of black un-styled content upon load. | |
| 102 | setStyle( "-fx-background-color: white;" ); | |
| 103 | ||
| 104 | invokeLater( () -> { | |
| 105 | mView = new HtmlPanel(); | |
| 106 | mScrollPane = new JScrollPane( mView ); | |
| 107 | ||
| 108 | // Enabling the cache attempts to prevent black flashes when resizing. | |
| 109 | setCache( true ); | |
| 110 | setCacheHint( SPEED ); | |
| 111 | setContent( mScrollPane ); | |
| 112 | ||
| 113 | final var context = mView.getSharedContext(); | |
| 114 | final var textRenderer = context.getTextRenderer(); | |
| 115 | context.setReplacedElementFactory( FACTORY ); | |
| 116 | textRenderer.setSmoothingThreshold( 0 ); | |
| 117 | ||
| 118 | localeProperty().addListener( ( c, o, n ) -> { | |
| 119 | mLocaleUrl = toUrl( getLocale() ); | |
| 120 | rerender(); | |
| 121 | } ); | |
| 122 | ||
| 123 | fontFamilyProperty().addListener( ( c, o, n ) -> rerender() ); | |
| 124 | fontSizeProperty().addListener( ( c, o, n ) -> rerender() ); | |
| 125 | } ); | |
| 126 | } | |
| 127 | ||
| 128 | /** | |
| 129 | * Updates the internal HTML source shown in the preview pane. | |
| 130 | * | |
| 131 | * @param html The new HTML document to display. | |
| 132 | */ | |
| 133 | public void render( final String html ) { | |
| 134 | mView.render( decorate( html ), getBaseUri() ); | |
| 135 | } | |
| 136 | ||
| 137 | /** | |
| 138 | * Clears the caches then rerenders the content. | |
| 139 | */ | |
| 140 | public void refresh() { | |
| 141 | FACTORY.clearCache(); | |
| 142 | rerender(); | |
| 143 | } | |
| 144 | ||
| 145 | private void rerender() { | |
| 146 | render( mHtmlDocument.toString() ); | |
| 147 | } | |
| 148 | ||
| 149 | /** | |
| 150 | * Attaches the HTML head prefix and HTML tail suffix to the given HTML | |
| 151 | * string. | |
| 152 | * | |
| 153 | * @param html The HTML to adorn with opening and closing tags. | |
| 154 | * @return A complete HTML document, ready for rendering. | |
| 155 | */ | |
| 156 | private String decorate( final String html ) { | |
| 157 | mHtmlDocument.setLength( 0 ); | |
| 158 | mHtmlDocument.append( head() ); | |
| 159 | mHtmlDocument.append( html ); | |
| 160 | mHtmlDocument.append( tail() ); | |
| 161 | return mHtmlDocument.toString(); | |
| 162 | } | |
| 163 | ||
| 164 | private String head() { | |
| 165 | return format( | |
| 166 | HTML_HEAD, | |
| 167 | getLocale().getLanguage(), | |
| 168 | format( HTML_STYLESHEET, HTML_STYLE_PREVIEW ), | |
| 169 | mLocaleUrl == null ? "" : format( HTML_STYLESHEET, mLocaleUrl ), | |
| 170 | getFontFamily(), | |
| 171 | (int) (getFontSize() * (1 + 1 / 3f)), | |
| 172 | mBaseUriPath | |
| 173 | ); | |
| 174 | } | |
| 175 | ||
| 176 | private String tail() { | |
| 177 | return HTML_TAIL; | |
| 178 | } | |
| 179 | ||
| 180 | /** | |
| 181 | * Clears the preview pane by rendering an empty string. | |
| 182 | */ | |
| 183 | public void clear() { | |
| 184 | render( "" ); | |
| 185 | } | |
| 186 | ||
| 187 | /** | |
| 188 | * Sets the base URI to the containing directory the file being edited. | |
| 189 | * | |
| 190 | * @param path The path to the file being edited. | |
| 191 | */ | |
| 192 | public void setBaseUri( final Path path ) { | |
| 193 | final var parent = path.getParent(); | |
| 194 | mBaseUriPath = parent == null ? "" : parent.toUri().toString(); | |
| 195 | } | |
| 196 | ||
| 197 | /** | |
| 198 | * Scrolls to the closest element matching the given identifier without | |
| 199 | * waiting for the document to be ready. | |
| 200 | * | |
| 201 | * @param id Scroll the preview pane to this unique paragraph identifier. | |
| 202 | */ | |
| 203 | public void scrollTo( final String id ) { | |
| 204 | final Runnable scrollToBox = () -> { | |
| 205 | int iter = 0; | |
| 206 | Box box = null; | |
| 207 | ||
| 208 | while( iter++ < 3 && ((box = mView.getBoxById( id )) == null) ) { | |
| 209 | try { | |
| 210 | sleep( 10 ); | |
| 211 | } catch( final Exception ex ) { | |
| 212 | clue( ex ); | |
| 213 | } | |
| 214 | } | |
| 215 | ||
| 216 | scrollTo( box ); | |
| 217 | }; | |
| 218 | ||
| 219 | if( Platform.isFxApplicationThread() ) { | |
| 220 | scrollToBox.run(); | |
| 221 | } | |
| 222 | else { | |
| 223 | runLater( scrollToBox ); | |
| 224 | } | |
| 225 | } | |
| 226 | ||
| 227 | /** | |
| 228 | * Scrolls to the location specified by the {@link Box} that corresponds | |
| 229 | * to a point somewhere in the preview pane. If there is no caret, then | |
| 230 | * this will not change the scroll position. Changing the scroll position | |
| 231 | * to the top if the {@link Box} instance is {@code null} will result in | |
| 232 | * jumping around a lot and inconsistent synchronization issues. | |
| 233 | * | |
| 234 | * @param box The rectangular region containing the caret, or {@code null} | |
| 235 | * if the HTML does not have a caret. | |
| 236 | */ | |
| 237 | private void scrollTo( final Box box ) { | |
| 238 | if( box != null ) { | |
| 239 | scrollTo( createPoint( box ) ); | |
| 240 | } | |
| 241 | } | |
| 242 | ||
| 243 | private void scrollTo( final Point point ) { | |
| 244 | invokeLater( () -> { | |
| 245 | mView.scrollTo( point ); | |
| 246 | getScrollPane().repaint(); | |
| 247 | } ); | |
| 248 | } | |
| 249 | ||
| 250 | /** | |
| 251 | * Creates a {@link Point} to use as a reference for scrolling to the area | |
| 252 | * described by the given {@link Box}. The {@link Box} coordinates are used | |
| 253 | * to populate the {@link Point}'s location, with minor adjustments for | |
| 254 | * vertical centering. | |
| 255 | * | |
| 256 | * @param box The {@link Box} that represents a scrolling anchor reference. | |
| 257 | * @return A coordinate suitable for scrolling to. | |
| 258 | */ | |
| 259 | private Point createPoint( final Box box ) { | |
| 260 | assert box != null; | |
| 261 | ||
| 262 | // Scroll back up by half the height of the scroll bar to keep the typing | |
| 263 | // area within the view port. Otherwise the view port will have jumped too | |
| 264 | // high up and the most recently typed letters won't be visible. | |
| 265 | int y = max( box.getAbsY() - getVerticalScrollBarHeight() / 2, 0 ); | |
| 266 | int x = box.getAbsX(); | |
| 267 | ||
| 268 | if( !box.getStyle().isInline() ) { | |
| 269 | final var margin = box.getMargin( mView.getLayoutContext() ); | |
| 270 | y += margin.top(); | |
| 271 | x += margin.left(); | |
| 272 | } | |
| 273 | ||
| 274 | return new Point( x, y ); | |
| 275 | } | |
| 276 | ||
| 277 | private String getBaseUri() { | |
| 278 | return mBaseUriPath; | |
| 279 | } | |
| 280 | ||
| 281 | private JScrollPane getScrollPane() { | |
| 282 | return mScrollPane; | |
| 283 | } | |
| 284 | ||
| 285 | public JScrollBar getVerticalScrollBar() { | |
| 286 | return getScrollPane().getVerticalScrollBar(); | |
| 287 | } | |
| 288 | ||
| 289 | private int getVerticalScrollBarHeight() { | |
| 290 | return getVerticalScrollBar().getHeight(); | |
| 291 | } | |
| 292 | ||
| 293 | /** | |
| 294 | * Returns the ISO 639 alpha-2 or alpha-3 language code followed by a hyphen | |
| 295 | * followed by the ISO 15924 alpha-4 script code, followed by an ISO 3166 | |
| 296 | * alpha-2 country code or UN M.49 numeric-3 area code. For example, this | |
| 297 | * could return "en-Latn-CA" for Canadian English written in the Latin | |
| 298 | * character set. | |
| 299 | * | |
| 300 | * @return Unique identifier for language and country. | |
| 301 | */ | |
| 302 | private static URL toUrl( final Locale locale ) { | |
| 303 | return toUrl( | |
| 304 | get( | |
| 305 | sSettings.getSetting( STYLESHEET_PREVIEW_LOCALE, "" ), | |
| 306 | locale.getLanguage(), | |
| 307 | locale.getScript(), | |
| 308 | locale.getCountry() | |
| 309 | ) | |
| 310 | ); | |
| 311 | } | |
| 312 | ||
| 313 | private static URL toUrl( final String path ) { | |
| 314 | return HtmlPreview.class.getResource( path ); | |
| 315 | } | |
| 316 | ||
| 317 | private Locale getLocale() { | |
| 318 | return localeProperty().toLocale(); | |
| 319 | } | |
| 320 | ||
| 321 | private LocaleProperty localeProperty() { | |
| 322 | return mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE ); | |
| 323 | } | |
| 324 | ||
| 325 | private String getFontFamily() { | |
| 326 | return fontFamilyProperty().get(); | |
| 327 | } | |
| 328 | ||
| 329 | private StringProperty fontFamilyProperty() { | |
| 330 | return mWorkspace.stringProperty( KEY_UI_FONT_PREVIEW_NAME ); | |
| 331 | } | |
| 332 | ||
| 333 | private double getFontSize() { | |
| 334 | return fontSizeProperty().get(); | |
| 335 | } | |
| 336 | ||
| 337 | /** | |
| 338 | * Returns the font size in points. | |
| 339 | * @return The user-defined font size (in pt). | |
| 340 | */ | |
| 319 | 341 | private DoubleProperty fontSizeProperty() { |
| 320 | 342 | return mWorkspace.doubleProperty( KEY_UI_FONT_PREVIEW_SIZE ); |
| 18 | 18 | import java.util.zip.Deflater; |
| 19 | 19 | |
| 20 | import static com.keenwrite.Constants.DIAGRAM_SERVER_NAME; | |
| 20 | 21 | import static com.keenwrite.events.StatusEvent.clue; |
| 21 | 22 | import static com.keenwrite.processors.IdentityProcessor.IDENTITY; |
| ... | ||
| 97 | 98 | final var encoded = encode( text ); |
| 98 | 99 | final var source = format( |
| 99 | "https://kroki.io/%s/svg/%s", type, encoded ); | |
| 100 | "https://%s/%s/svg/%s", DIAGRAM_SERVER_NAME, type, encoded ); | |
| 100 | 101 | |
| 101 | 102 | final var link = context.resolveLink( LINK, source, false ); |
| 27 | 27 | @Subscribe |
| 28 | 28 | public void handle( final StatusEvent event ) { |
| 29 | final var message = event.toString(); | |
| 29 | final var message = event.getMessage(); | |
| 30 | 30 | |
| 31 | 31 | // Don't burden the repaint thread if there's no status bar change. |
| 16 | 16 | import java.util.TreeSet; |
| 17 | 17 | |
| 18 | import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE; | |
| 18 | 19 | import static com.keenwrite.Constants.ICON_DIALOG; |
| 19 | 20 | import static com.keenwrite.Messages.get; |
| 20 | 21 | import static com.keenwrite.events.Bus.register; |
| 21 | 22 | import static com.keenwrite.events.StatusEvent.clue; |
| 23 | import static java.nio.file.Files.createTempFile; | |
| 24 | import static java.nio.file.Files.write; | |
| 25 | import static java.nio.file.StandardOpenOption.APPEND; | |
| 26 | import static java.nio.file.StandardOpenOption.CREATE; | |
| 22 | 27 | import static java.time.LocalDateTime.now; |
| 23 | 28 | import static java.time.format.DateTimeFormatter.ofPattern; |
| 29 | import static javafx.application.Platform.runLater; | |
| 24 | 30 | import static javafx.collections.FXCollections.observableArrayList; |
| 25 | 31 | import static javafx.event.ActionEvent.ACTION; |
| ... | ||
| 60 | 66 | @Subscribe |
| 61 | 67 | public void log( final StatusEvent event ) { |
| 62 | final var logEntry = new LogEntry( event ); | |
| 68 | runLater( () ->{ | |
| 69 | final var logEntry = new LogEntry( event ); | |
| 63 | 70 | |
| 64 | if( !mEntries.contains( logEntry ) ) { | |
| 65 | mEntries.add( logEntry ); | |
| 71 | if( !mEntries.contains( logEntry ) ) { | |
| 72 | mEntries.add( logEntry ); | |
| 66 | 73 | |
| 67 | while( mEntries.size() > CACHE_SIZE ) { | |
| 68 | mEntries.remove( 0 ); | |
| 69 | } | |
| 74 | while( mEntries.size() > CACHE_SIZE ) { | |
| 75 | mEntries.remove( 0 ); | |
| 76 | } | |
| 70 | 77 | |
| 71 | mTable.scrollTo( logEntry ); | |
| 72 | } | |
| 78 | mTable.scrollTo( logEntry ); | |
| 79 | } | |
| 80 | }); | |
| 73 | 81 | } |
| 74 | 82 | |
| ... | ||
| 164 | 172 | public LogEntry( final StatusEvent event ) { |
| 165 | 173 | mDate = new SimpleStringProperty( toString( now() ) ); |
| 166 | mMessage = new SimpleStringProperty( event.toString() ); | |
| 174 | mMessage = new SimpleStringProperty( event.getMessage() ); | |
| 167 | 175 | mTrace = new SimpleStringProperty( event.getProblem() ); |
| 168 | 176 | } |
| ... | ||
| 180 | 188 | } |
| 181 | 189 | |
| 182 | private String toString( final LocalDateTime date ) { | |
| 183 | return date.format( ofPattern( "d MMM u HH:mm:ss" ) ); | |
| 190 | /** | |
| 191 | * Call from constructor to save log message for debugging purposes. | |
| 192 | */ | |
| 193 | @SuppressWarnings( "unused" ) | |
| 194 | private void persist() { | |
| 195 | try { | |
| 196 | final var file = createTempFile( APP_TITLE_LOWERCASE, ".log" ); | |
| 197 | write( file, toString().getBytes(), CREATE, APPEND ); | |
| 198 | } catch( final Exception ignored ) { | |
| 199 | System.out.println( toString() ); | |
| 200 | } | |
| 184 | 201 | } |
| 185 | 202 | |
| ... | ||
| 195 | 212 | public int hashCode() { |
| 196 | 213 | return mMessage != null ? mMessage.hashCode() : 0; |
| 214 | } | |
| 215 | ||
| 216 | @Override | |
| 217 | public String toString() { | |
| 218 | final var date = mDate == null ? "" : mDate.get(); | |
| 219 | final var message = mMessage == null ? "" : mMessage.get(); | |
| 220 | final var trace = mTrace == null ? "" : mTrace.get(); | |
| 221 | ||
| 222 | return "LogEntry{" + | |
| 223 | "mDate=" + (date == null ? "''" : date) + | |
| 224 | ", mMessage=" + (message == null ? "''" : message) + | |
| 225 | ", mTrace=" + (trace == null ? "''" : trace) + | |
| 226 | '}'; | |
| 227 | } | |
| 228 | ||
| 229 | private String toString( final LocalDateTime date ) { | |
| 230 | return date.format( ofPattern( "d MMM u HH:mm:ss" ) ); | |
| 197 | 231 | } |
| 198 | 232 | } |
| 50 | 50 | Main.status.image.request.init=Initializing HTTP request |
| 51 | 51 | Main.status.image.request.fetch=Requesting content type from {0} |
| 52 | Main.status.image.request.success=Detected content type ''{0}'' | |
| 52 | Main.status.image.request.success=Determined content type ''{0}'' | |
| 53 | Main.status.image.request.error.media=No media type for ''{0}'' | |
| 54 | Main.status.image.request.error.cert=Could not accept certificate for ''{0}'' | |
| 53 | 55 | |
| 54 | 56 | Main.status.font.search.missing=No font name starting with ''{0}'' was found |
| 1 | html{box-sizing:border-box;font-size:12pt}body,h1,h2,h3,h4,h5,h6,ol,p,ul{margin:0;padding:0}img{max-width:100%;height:auto}table{table-collapse:collapse;table-spacing:0;border-spacing:0} | |
| 1 | body,h1,h2,h3,h4,h5,h6,ol,p,ul{margin:0;padding:0}img{max-width:100%;height:auto}table{table-collapse:collapse;table-spacing:0;border-spacing:0} | |
| 2 | 2 | |
| 3 | /* Do not use points (pt): FlyingSaucer on Debian fails to render. */ | |
| 3 | 4 | body { |
| 4 | /* Noto Serif introduces whitespace on style transitions. */ | |
| 5 | font-family: 'Source Serif Pro'; | |
| 6 | font-size: 12pt; | |
| 7 | ||
| 8 | 5 | background-color: #fff; |
| 9 | 6 | margin: 0 auto; |
| 10 | 7 | line-height: 1.6; |
| 11 | 8 | color: #454545; |
| 12 | padding: 1em; | |
| 9 | padding: .5em; | |
| 13 | 10 | font-feature-settings: 'liga' 1; |
| 14 | 11 | font-variant-ligatures: normal; |
| ... | ||
| 42 | 39 | |
| 43 | 40 | h1 { |
| 44 | font-size: 21pt; | |
| 41 | font-size: 28px; | |
| 45 | 42 | } |
| 46 | 43 | |
| 47 | 44 | h2 { |
| 48 | font-size: 18pt; | |
| 45 | font-size: 24px; | |
| 49 | 46 | border-bottom: 1px solid #ccc; |
| 50 | 47 | } |
| 51 | 48 | |
| 52 | 49 | h3 { |
| 53 | font-size: 15pt; | |
| 50 | font-size: 20px; | |
| 54 | 51 | } |
| 55 | 52 | |
| 56 | 53 | h4 { |
| 57 | font-size: 13.5pt; | |
| 54 | font-size: 18px; | |
| 58 | 55 | } |
| 59 | 56 | |
| 60 | 57 | h5 { |
| 61 | font-size: 12pt; | |
| 58 | font-size: 16px; | |
| 62 | 59 | } |
| 63 | 60 | |
| 64 | 61 | h6 { |
| 65 | font-size: 10.5pt; | |
| 62 | font-size: 14px; | |
| 66 | 63 | } |
| 67 | 64 | |
| 1 | #!/usr/bin/env bash | |
| 2 | ||
| 3 | # Writes the name for all OTF files found in the current directory or lower | |
| 4 | ||
| 5 | find . -type f \( -name "*otf" -o -name "*ttf" \) -exec \ | |
| 6 | fc-scan --format "%{foundry}: %{family}\n" {} \; | uniq | sort | |
| 7 | ||
| 8 | 1 |