| Author | DaveJarvis <email> |
|---|---|
| Date | 2020-06-19 23:31:03 GMT-0700 |
| Commit | 58a1faee8dddd8054fbfe3fcac01001fab87d619 |
| Parent | 5eeed00 |
| Delta | 120 lines added, 111 lines removed, 9-line increase |
| -/* | ||
| -This software is released under the MIT license: | ||
| - | ||
| -Permission is hereby granted, free of charge, to any person obtaining a copy of | ||
| -this software and associated documentation files (the "Software"), to deal in | ||
| -the Software without restriction, including without limitation the rights to | ||
| -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of | ||
| -the Software, and to permit persons to whom the Software is furnished to do so, | ||
| -subject to the following conditions: | ||
| - | ||
| -The above copyright notice and this permission notice shall be included in all | ||
| -copies or substantial portions of the Software. | ||
| - | ||
| -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS | ||
| -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | ||
| -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER | ||
| -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | ||
| -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | ||
| -*/ | ||
| - | ||
| -/* Source: https://github.com/nicolashery/markdownpad-github */ | ||
| - | ||
| -/* GitHub stylesheet for MarkdownPad (http://markdownpad.com) */ | ||
| - | ||
| -/* RESET | ||
| -=============================================================================*/ | ||
| - | ||
| -html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, | ||
| -p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, | ||
| -em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, | ||
| -b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, | ||
| -legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, | ||
| -details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, | ||
| -ruby, section, summary, time, mark, audio, video { | ||
| - margin: 0; | ||
| - padding: 0; | ||
| - border: 0; | ||
| -} | ||
| - | ||
| /* BODY | ||
| =============================================================================*/ | ||
| - | ||
| body { | ||
| - font-family: serif; | ||
| - font-size: 14px; | ||
| - line-height: 1.6; | ||
| - color: #333; | ||
| + font-family: Vollkorn, serif; | ||
| + font-size: 16px; | ||
| background-color: #fff; | ||
| - padding: 20px; | ||
| - max-width: 960px; | ||
| margin: 0 auto; | ||
| + max-width: 960px; | ||
| + line-height: 1.6; | ||
| + color: #454545; | ||
| + padding: 0 10px | ||
| } | ||
| p, blockquote, ul, ol, dl, table, pre { | ||
| - margin: 15px 0; | ||
| + margin: 20px 0; | ||
| } | ||
| /* HEADERS | ||
| =============================================================================*/ | ||
| h1, h2, h3, h4, h5, h6 { | ||
| margin: 20px 0 10px; | ||
| padding: 0; | ||
| - font-weight: bold; | ||
| - -webkit-font-smoothing: antialiased; | ||
| } | ||
| h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code, | ||
| h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code { | ||
| font-size: inherit; | ||
| } | ||
| h1 { | ||
| font-size: 28px; | ||
| - color: #000; | ||
| } | ||
| h2 { | ||
| font-size: 24px; | ||
| border-bottom: 1px solid #ccc; | ||
| - color: #000; | ||
| } | ||
| h3 { | ||
| - font-size: 18px; | ||
| + font-size: 20px; | ||
| } | ||
| h4 { | ||
| - font-size: 16px; | ||
| + font-size: 18px; | ||
| } | ||
| h5 { | ||
| - font-size: 14px; | ||
| + font-size: 16px; | ||
| } | ||
| h6 { | ||
| - color: #777; | ||
| font-size: 14px; | ||
| } | ||
| a { | ||
| - color: #4183C4; | ||
| + color: #0077aa; | ||
| text-decoration: none; | ||
| } | ||
| ul, ol { | ||
| - padding-left: 30px; | ||
| + padding-left: 20px; | ||
| } | ||
| padding: 0px 0px; | ||
| white-space: nowrap; | ||
| - border: 1px solid #eaeaea; | ||
| + border: 1px solid #eee; | ||
| background-color: #f8f8f8; | ||
| border-radius: 3px; | ||
| blockquote { | ||
| - border-left: 4px solid #DDD; | ||
| + border-left: 5px solid #DDD; | ||
| padding: 0 15px; | ||
| color: #777; | ||
| hr { | ||
| clear: both; | ||
| - margin: 15px 0; | ||
| + margin: 10px 0; | ||
| height: 0px; | ||
| overflow: hidden; | ||
| border: none; | ||
| background: transparent; | ||
| - border-bottom: 4px solid #ddd; | ||
| + border-bottom: 4px solid #eee; | ||
| padding: 0; | ||
| } | ||
| /* TABLES | ||
| =============================================================================*/ | ||
| -table th { | ||
| - font-weight: bold; | ||
| +table { | ||
| + width: 100%; | ||
| + border-collapse: collapse; | ||
| } | ||
| -table th, table td { | ||
| - border: 1px solid #ccc; | ||
| - padding: 6px 13px; | ||
| +tr:nth-child(odd) { | ||
| + background-color: #eee; | ||
| } | ||
| -table tr { | ||
| - border-top: 1px solid #ccc; | ||
| - background-color: #fff; | ||
| +th { | ||
| + background-color: #454545; | ||
| + color: #fff; | ||
| } | ||
| -table tr:nth-child(2n) { | ||
| - background-color: #f8f8f8; | ||
| +th, td { | ||
| + text-align: left; | ||
| + padding: 0 1em; | ||
| } | ||
| /* IMAGES | ||
| =============================================================================*/ | ||
| img { | ||
| - max-width: 100% | ||
| + max-width: 100%; | ||
| + height: auto; | ||
| } | ||
| */ | ||
| public final class HTMLPreviewPane extends Pane { | ||
| + /** | ||
| + * Prevent scrolling to the top on every key press. | ||
| + */ | ||
| private static class HTMLPanel extends XHTMLPanel { | ||
| - /** | ||
| - * Prevent scrolling to the top. | ||
| - */ | ||
| @Override | ||
| public void resetScrollPosition() { | ||
| */ | ||
| public HTMLPreviewPane() { | ||
| - final ChainedReplacedElementFactory factory = | ||
| - new ChainedReplacedElementFactory(); | ||
| + final var factory = new ChainedReplacedElementFactory(); | ||
| factory.addFactory( new SVGReplacedElementFactory() ); | ||
| factory.addFactory( new SwingReplacedElementFactory() ); | ||
| public void update( final String html ) { | ||
| final Document jsoupDoc = Jsoup.parse( decorate( html ) ); | ||
| - org.w3c.dom.Document w3cDoc = mW3cDom.fromJsoup( jsoupDoc ); | ||
| + final org.w3c.dom.Document w3cDoc = mW3cDom.fromJsoup( jsoupDoc ); | ||
| mRenderer.setDocument( w3cDoc, getBaseUrl(), mNamespaceHandler ); | ||
| } | ||
| private String decorate( final String html ) { | ||
| + // Trim the HTML back to the header. | ||
| mHtml.setLength( mHtmlPrefixLength ); | ||
| + | ||
| + // Write the HTML body element followed by closing tags. | ||
| return mHtml.append( html ) | ||
| .append( HTML_FOOTER ) | ||
| public void clear() { | ||
| update( "" ); | ||
| - } | ||
| - | ||
| - private String getBaseUrl() { | ||
| - final Path basePath = getPath(); | ||
| - final Path parent = basePath == null ? null : basePath.getParent(); | ||
| - | ||
| - return parent == null ? "" : parent.toUri().toString(); | ||
| } | ||
| public JScrollBar getVerticalScrollBar() { | ||
| return getScrollPane().getVerticalScrollBar(); | ||
| + } | ||
| + | ||
| + private String getBaseUrl() { | ||
| + final Path basePath = getPath(); | ||
| + final Path parent = basePath == null ? null : basePath.getParent(); | ||
| + | ||
| + return parent == null ? "" : parent.toUri().toString(); | ||
| } | ||
| } | ||
| import java.util.Map; | ||
| +import static java.awt.Color.RED; | ||
| import static java.awt.Color.WHITE; | ||
| import static java.awt.RenderingHints.*; | ||
| +import static java.awt.image.BufferedImage.TYPE_INT_RGB; | ||
| import static org.apache.batik.transcoder.SVGAbstractTranscoder.KEY_WIDTH; | ||
| import static org.apache.batik.transcoder.image.ImageTranscoder.KEY_BACKGROUND_COLOR; | ||
| } | ||
| - public static BufferedImage rasterize( final String url, final int width ) | ||
| - throws IOException, TranscoderException { | ||
| - return rasterize( new URL( url ), width ); | ||
| + /** | ||
| + * Rasterizes the vector graphic file at the given URL. If any exception | ||
| + * happens, a red circle is returned instead. | ||
| + * | ||
| + * @param url The URL to a vector graphic file, which must include the | ||
| + * protocol scheme (such as file:// or https://). | ||
| + * @param width The number of pixels wide to render the image. The aspect | ||
| + * ratio is maintained. | ||
| + * @return Either the rasterized image upon success or a red circle. | ||
| + */ | ||
| + public static Image rasterize( final String url, final int width ) { | ||
| + try { | ||
| + return rasterize( new URL( url ), width ); | ||
| + } catch( final Exception e ) { | ||
| + return createPlaceholderImage( width ); | ||
| + } | ||
| } | ||
| return transcoder.getBufferedImage(); | ||
| + } | ||
| + | ||
| + @SuppressWarnings("SuspiciousNameCombination") | ||
| + private static Image createPlaceholderImage( final int width ) { | ||
| + final var image = new BufferedImage( width, width, TYPE_INT_RGB ); | ||
| + final var graphics = (Graphics2D) image.getGraphics(); | ||
| + | ||
| + graphics.setColor( RED ); | ||
| + graphics.setStroke( new BasicStroke( 5 ) ); | ||
| + graphics.drawOval( 5, 5, width / 2, width / 2 ); | ||
| + | ||
| + return image; | ||
| } | ||
| } | ||
| import java.awt.*; | ||
| +import java.util.LinkedHashMap; | ||
| +import java.util.Map; | ||
| + | ||
| +import static com.scrivenvar.preview.SVGRasterizer.rasterize; | ||
| public class SVGReplacedElementFactory | ||
| private static final String HTML_IMAGE_SRC = "src"; | ||
| - public ReplacedElement createReplacedElement( | ||
| - final LayoutContext c, final BlockBox box, final UserAgentCallback uac, | ||
| - final int cssWidth, final int cssHeight ) { | ||
| - final Element e = box.getElement(); | ||
| + /** | ||
| + * Constrain memory. | ||
| + */ | ||
| + private static final int MAX_CACHED_IMAGES = 100; | ||
| - if( e == null ) { | ||
| - return null; | ||
| + /** | ||
| + * Where to put document inline evaluated R expressions. | ||
| + */ | ||
| + private final Map<String, Image> mImageCache = new LinkedHashMap<>() { | ||
| + @Override | ||
| + protected boolean removeEldestEntry( | ||
| + final Map.Entry<String, Image> eldest ) { | ||
| + return size() > MAX_CACHED_IMAGES; | ||
| } | ||
| + }; | ||
| - final String nodeName = e.getNodeName(); | ||
| - ReplacedElement result = null; | ||
| + public ReplacedElement createReplacedElement( | ||
| + final LayoutContext c, | ||
| + final BlockBox box, | ||
| + final UserAgentCallback uac, | ||
| + final int cssWidth, | ||
| + final int cssHeight ) { | ||
| + final Element e = box.getElement(); | ||
| - if( HTML_IMAGE.equals( nodeName ) ) { | ||
| - final String src = e.getAttribute( HTML_IMAGE_SRC ); | ||
| - final String ext = FilenameUtils.getExtension( src ); | ||
| + if( e != null ) { | ||
| + final String nodeName = e.getNodeName(); | ||
| - if( SVG_FILE.equalsIgnoreCase( ext ) ) { | ||
| - try { | ||
| - final int width = box.getContentWidth(); | ||
| - final Image image = SVGRasterizer.rasterize( src, width ); | ||
| + if( HTML_IMAGE.equals( nodeName ) ) { | ||
| + final String src = e.getAttribute( HTML_IMAGE_SRC ); | ||
| + final String ext = FilenameUtils.getExtension( src ); | ||
| - final int w = image.getWidth( null ); | ||
| - final int h = image.getHeight( null ); | ||
| + if( SVG_FILE.equalsIgnoreCase( ext ) ) { | ||
| + try { | ||
| + final int width = box.getContentWidth(); | ||
| + final Image image = getImage( src, width ); | ||
| - result = new ImageReplacedElement( image, w, h ); | ||
| - } catch( final Exception ex ) { | ||
| - getNotifier().notify( ex ); | ||
| + final int w = image.getWidth( null ); | ||
| + final int h = image.getHeight( null ); | ||
| + | ||
| + return new ImageReplacedElement( image, w, h ); | ||
| + } catch( final Exception ex ) { | ||
| + getNotifier().notify( ex ); | ||
| + } | ||
| } | ||
| } | ||
| } | ||
| - return result; | ||
| + return null; | ||
| } | ||
| @Override | ||
| public void reset() { | ||
| } | ||
| @Override | ||
| - public void remove( Element e ) { | ||
| + public void remove( final Element e ) { | ||
| } | ||
| @Override | ||
| public void setFormSubmissionListener( FormSubmissionListener listener ) { | ||
| + } | ||
| + | ||
| + private Image getImage( final String src, final int width ) { | ||
| + return mImageCache.computeIfAbsent( src, v -> rasterize( src, width ) ); | ||
| } | ||