| 1 | # ----------------------------------------------------------------------------- | |
| 2 | # Copyright 2020, White Magic Software, Ltd. | |
| 3 | # | |
| 4 | # Permission is hereby granted, free of charge, to any person obtaining | |
| 5 | # a copy of this software and associated documentation files (the | |
| 6 | # "Software"), to deal in the Software without restriction, including | |
| 7 | # without limitation the rights to use, copy, modify, merge, publish, | |
| 8 | # distribute, sublicense, and/or sell copies of the Software, and to | |
| 9 | # permit persons to whom the Software is furnished to do so, subject to | |
| 10 | # the following conditions: | |
| 11 | # | |
| 12 | # The above copyright notice and this permission notice shall be | |
| 13 | # included in all copies or substantial portions of the Software. | |
| 14 | # | |
| 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
| 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
| 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | |
| 19 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |
| 20 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
| 21 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| 22 | # ----------------------------------------------------------------------------- | |
| 23 | ||
| 24 | # ----------------------------------------------------------------------------- | |
| 25 | # Substitute R expressions in a document with their evaluated value. The | |
| 26 | # anchor variable must be set for functions that use relative dates. | |
| 27 | # ----------------------------------------------------------------------------- | |
| 28 | ||
| 29 | # ----------------------------------------------------------------------------- | |
| 30 | # Evaluates an expression; writes s if there is no expression. | |
| 31 | # ----------------------------------------------------------------------------- | |
| 32 | x <- function( s ) { | |
| 33 | tryCatch( { | |
| 34 | r = eval( parse( text = s ) ) | |
| 35 | ||
| 36 | # If the result isn't primitive, then it was probably parsed into | |
| 37 | # an unprintable object (e.g., "gray" becomes a colour). In those | |
| 38 | # cases, return the original text string. Otherwise, an atomic | |
| 39 | # value means a primitive type (string, integer, etc.) that can be | |
| 40 | # written directly into the document. | |
| 41 | ifelse( is.atomic( r ), r, s ); | |
| 42 | }, | |
| 43 | warning = function( w ) { s }, | |
| 44 | error = function( e ) { s } ) | |
| 45 | } | |
| 46 | ||
| 47 | # ----------------------------------------------------------------------------- | |
| 48 | # Returns a date offset by a given number of days, relative to the given | |
| 49 | # date (d). This does not use the anchor, but is used to get the anchor's | |
| 50 | # value as a date. | |
| 51 | # ----------------------------------------------------------------------------- | |
| 52 | when <- function( d, n = 0, format = "%Y-%m-%d" ) { | |
| 53 | as.Date( d, format = format ) + x( n ) | |
| 54 | } | |
| 55 | ||
| 56 | # ----------------------------------------------------------------------------- | |
| 57 | # Full date (s) offset by an optional number of days before or after. | |
| 58 | # This will remove leading zeros (applying leading spaces instead, which | |
| 59 | # are ignored by any worthwhile typesetting engine). | |
| 60 | # ----------------------------------------------------------------------------- | |
| 61 | annal <- function( days = 0, format = "%Y-%m-%d", oformat = "%B %d, %Y" ) { | |
| 62 | format( when( anchor, days ), format = oformat ) | |
| 63 | } | |
| 64 | ||
| 65 | # ----------------------------------------------------------------------------- | |
| 66 | # Extracts the year from a date string. | |
| 67 | # ----------------------------------------------------------------------------- | |
| 68 | year <- function( days = 0, format = "%Y-%m-%d" ) { | |
| 69 | annal( days, format, "%Y" ) | |
| 70 | } | |
| 71 | ||
| 72 | # ----------------------------------------------------------------------------- | |
| 73 | # Day of the week (in days since the anchor date). | |
| 74 | # ----------------------------------------------------------------------------- | |
| 75 | weekday <- function( n ) { | |
| 76 | weekdays( when( anchor, n ) ) | |
| 77 | } | |
| 78 | ||
| 79 | # ----------------------------------------------------------------------------- | |
| 80 | # String concatenate function alias because paste0 is a terrible name. | |
| 81 | # ----------------------------------------------------------------------------- | |
| 82 | concat <- paste0 | |
| 83 | ||
| 84 | # ----------------------------------------------------------------------------- | |
| 85 | # Translates a number from digits to words using Chicago Manual of Style. | |
| 86 | # This does not translate numbers greater than one hundred. If ordinal | |
| 87 | # is TRUE, this will return the ordinal name. This will not produce ordinals | |
| 88 | # for numbers greater than 100. | |
| 89 | # ----------------------------------------------------------------------------- | |
| 90 | cms <- function( n, ordinal = FALSE ) { | |
| 91 | n <- x( n ) | |
| 92 | ||
| 93 | if( n == 0 ) { | |
| 94 | if( ordinal ) { | |
| 95 | return( "zeroth" ) | |
| 96 | } | |
| 97 | ||
| 98 | return( "zero" ) | |
| 99 | } | |
| 100 | ||
| 101 | # Concatenate this a little later. | |
| 102 | if( n < 0 ) { | |
| 103 | result = "negative " | |
| 104 | n = abs( n ) | |
| 105 | } | |
| 106 | ||
| 107 | # Do not spell out numbers greater than one hundred. | |
| 108 | if( n > 100 ) { | |
| 109 | # Comma-separated numbers. | |
| 110 | return( commas( n ) ) | |
| 111 | } | |
| 112 | ||
| 113 | # Don't go beyond 100. | |
| 114 | if( n == 100 ) { | |
| 115 | if( ordinal ) { | |
| 116 | return( "one hundredth" ) | |
| 117 | } | |
| 118 | ||
| 119 | return( "one hundred" ) | |
| 120 | } | |
| 121 | ||
| 122 | # Samuel Langhorne Clemens noted English has too many exceptions. | |
| 123 | small = c( | |
| 124 | "one", "two", "three", "four", "five", | |
| 125 | "six", "seven", "eight", "nine", "ten", | |
| 126 | "eleven", "twelve", "thirteen", "fourteen", "fifteen", | |
| 127 | "sixteen", "seventeen", "eighteen", "nineteen" | |
| 128 | ) | |
| 129 | ||
| 130 | ord_small = c( | |
| 131 | "first", "second", "third", "fourth", "fifth", | |
| 132 | "sixth", "seventh", "eighth", "ninth", "tenth", | |
| 133 | "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth", | |
| 134 | "sixteenth", "seventeenth", "eighteenth", "nineteenth", "twentieth" | |
| 135 | ) | |
| 136 | ||
| 137 | # After this, the number (n) is between 20 and 99. | |
| 138 | if( n < 20 ) { | |
| 139 | if( ordinal ) { | |
| 140 | return( .subset( ord_small, n %% 100 ) ) | |
| 141 | } | |
| 142 | ||
| 143 | return( .subset( small, n %% 100 ) ) | |
| 144 | } | |
| 145 | ||
| 146 | tens = c( "", | |
| 147 | "twenty", "thirty", "forty", "fifty", | |
| 148 | "sixty", "seventy", "eighty", "ninety" | |
| 149 | ) | |
| 150 | ||
| 151 | ord_tens = c( "", | |
| 152 | "twentieth", "thirtieth", "fortieth", "fiftieth", | |
| 153 | "sixtieth", "seventieth", "eightieth", "ninetieth" | |
| 154 | ) | |
| 155 | ||
| 156 | ones_index = n %% 10 | |
| 157 | n = n %/% 10 | |
| 158 | ||
| 159 | # No number in the ones column, so the number must be a multiple of ten. | |
| 160 | if( ones_index == 0 ) { | |
| 161 | if( ordinal ) { | |
| 162 | return( .subset( ord_tens, n ) ) | |
| 163 | } | |
| 164 | ||
| 165 | return( .subset( tens, n ) ) | |
| 166 | } | |
| 167 | ||
| 168 | # Find the value from the ones column. | |
| 169 | if( ordinal ) { | |
| 170 | unit_1 = .subset( ord_small, ones_index ) | |
| 171 | } | |
| 172 | else { | |
| 173 | unit_1 = .subset( small, ones_index ) | |
| 174 | } | |
| 175 | ||
| 176 | # Find the tens column. | |
| 177 | unit_10 = .subset( tens, n ) | |
| 178 | ||
| 179 | # Hyphenate the tens and the ones together. | |
| 180 | concat( unit_10, concat( "-", unit_1 ) ) | |
| 181 | } | |
| 182 | ||
| 183 | # ----------------------------------------------------------------------------- | |
| 184 | # Returns a number as a comma-delimited string. This is a work-around | |
| 185 | # until Renjin fixes https://github.com/bedatadriven/renjin/issues/338 | |
| 186 | # ----------------------------------------------------------------------------- | |
| 187 | commas <- function( n ) { | |
| 188 | n <- x( n ) | |
| 189 | ||
| 190 | s <- sprintf( "%03.0f", n %% 1000 ) | |
| 191 | n <- n %/% 1000 | |
| 192 | ||
| 193 | while( n > 0 ) { | |
| 194 | s <- concat( sprintf( "%03.0f", n %% 1000 ), ',', s ) | |
| 195 | n <- n %/% 1000 | |
| 196 | } | |
| 197 | ||
| 198 | gsub( '^0*', '', s ) | |
| 199 | } | |
| 200 | ||
| 201 | # ----------------------------------------------------------------------------- | |
| 202 | # Returns a human-readable string that provides the elapsed time between | |
| 203 | # two numbers in terms of years, months, and days. If any unit value is zero, | |
| 204 | # the unit is not included. The words (year, month, day) are pluralised | |
| 205 | # according to English grammar. The numbers are written out according to | |
| 206 | # Chicago Manual of Style. This applies the serial comma. | |
| 207 | # | |
| 208 | # Both numbers are offsets relative to the anchor date. | |
| 209 | # | |
| 210 | # If all unit values are zero, this returns s ("same day" by default). | |
| 211 | # | |
| 212 | # If the start date (began) is greater than end date (ended), the dates are | |
| 213 | # swapped before calculations are performed. This allows any two dates | |
| 214 | # to be compared and positive unit values are always returned. | |
| 215 | # ----------------------------------------------------------------------------- | |
| 216 | elapsed <- function( began, ended, s = "same day" ) { | |
| 217 | began = when( anchor, began ) | |
| 218 | ended = when( anchor, ended ) | |
| 219 | ||
| 220 | # Swap the dates if the end date comes before the start date. | |
| 221 | if( as.integer( ended - began ) < 0 ) { | |
| 222 | tempd = began | |
| 223 | began = ended | |
| 224 | ended = tempd | |
| 225 | } | |
| 226 | ||
| 227 | # Calculate number of elapsed years. | |
| 228 | years = length( seq( from = began, to = ended, by = 'year' ) ) - 1 | |
| 229 | ||
| 230 | # Move the start date up by the number of elapsed years. | |
| 231 | if( years > 0 ) { | |
| 232 | began = seq( began, length = 2, by = concat( years, " years" ) )[2] | |
| 233 | years = pl.numeric( "year", years ) | |
| 234 | } | |
| 235 | else { | |
| 236 | # Zero years. | |
| 237 | years = "" | |
| 238 | } | |
| 239 | ||
| 240 | # Calculate number of elapsed months, excluding years. | |
| 241 | months = length( seq( from = began, to = ended, by = 'month' ) ) - 1 | |
| 242 | ||
| 243 | # Move the start date up by the number of elapsed months | |
| 244 | if( months > 0 ) { | |
| 245 | began = seq( began, length = 2, by = concat( months, " months" ) )[2] | |
| 246 | months = pl.numeric( "month", months ) | |
| 247 | } | |
| 248 | else { | |
| 249 | # Zero months | |
| 250 | months = "" | |
| 251 | } | |
| 252 | ||
| 253 | # Calculate number of elapsed days, excluding months and years. | |
| 254 | days = length( seq( from = began, to = ended, by = 'day' ) ) - 1 | |
| 255 | ||
| 256 | if( days > 0 ) { | |
| 257 | days = pl.numeric( "day", days ) | |
| 258 | } | |
| 259 | else { | |
| 260 | # Zero days | |
| 261 | days = "" | |
| 262 | } | |
| 263 | ||
| 264 | if( years <= 0 && months <= 0 && days <= 0 ) { | |
| 265 | return( s ) | |
| 266 | } | |
| 267 | ||
| 268 | # Put them all in a vector, then remove the empty values. | |
| 269 | s <- c( years, months, days ) | |
| 270 | s <- s[ s != "" ] | |
| 271 | ||
| 272 | r <- paste( s, collapse = ", " ) | |
| 273 | ||
| 274 | # If all three items are present, replace the last comma with ", and". | |
| 275 | if( length( s ) > 2 ) { | |
| 276 | return( gsub( "(.*),", "\\1, and", r ) ) | |
| 277 | } | |
| 278 | ||
| 279 | # Does nothing if no commas are present. | |
| 280 | gsub( "(.*),", "\\1 and", r ) | |
| 281 | } | |
| 282 | ||
| 283 | # ----------------------------------------------------------------------------- | |
| 284 | # Returns the number (n) in English followed by the plural or singular | |
| 285 | # form of the given string (s; resumably a noun), if applicable, according | |
| 286 | # to English grammar. That is, pl.numeric( "wolf", 5 ) will return | |
| 287 | # "five wolves". | |
| 288 | # ----------------------------------------------------------------------------- | |
| 289 | pl.numeric <- function( s, n ) { | |
| 290 | concat( cms( n ), concat( " ", pluralise( s, n ) ) ) | |
| 291 | } | |
| 292 | ||
| 293 | # ----------------------------------------------------------------------------- | |
| 294 | # Pluralise s if n is not equal to 1. | |
| 295 | # ----------------------------------------------------------------------------- | |
| 296 | pl <- function( s, n=2 ) { | |
| 297 | pluralize( s, x( n ) ) | |
| 298 | } | |
| 299 | ||
| 300 | # ----------------------------------------------------------------------------- | |
| 301 | # Name of the season, starting with an capital letter. | |
| 302 | # ----------------------------------------------------------------------------- | |
| 303 | season <- function( n, format = "%Y-%m-%d" ) { | |
| 304 | WS <- as.Date("2016-12-15", "%Y-%m-%d") # Winter Solstice | |
| 305 | SE <- as.Date("2016-03-15", "%Y-%m-%d") # Spring Equinox | |
| 306 | SS <- as.Date("2016-06-15", "%Y-%m-%d") # Summer Solstice | |
| 307 | AE <- as.Date("2016-09-15", "%Y-%m-%d") # Autumn Equinox | |
| 308 | ||
| 309 | d <- when( anchor, n ) | |
| 310 | d <- as.Date( strftime( d, format="2016-%m-%d" ) ) | |
| 311 | ||
| 312 | ifelse( d >= WS | d < SE, "Winter", | |
| 313 | ifelse( d >= SE & d < SS, "Spring", | |
| 314 | ifelse( d >= SS & d < AE, "Summer", "Autumn" ) | |
| 315 | ) | |
| 316 | ) | |
| 317 | } | |
| 318 | ||
| 319 | # ----------------------------------------------------------------------------- | |
| 320 | # Converts the first letter in a string to lowercase | |
| 321 | # ----------------------------------------------------------------------------- | |
| 322 | lc <- function( s ) { | |
| 323 | concat( tolower( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) ) | |
| 324 | } | |
| 325 | ||
| 326 | # ----------------------------------------------------------------------------- | |
| 327 | # Converts the entire string to lowercase | |
| 328 | # ----------------------------------------------------------------------------- | |
| 329 | lower <- tolower | |
| 330 | ||
| 331 | # ----------------------------------------------------------------------------- | |
| 332 | # Converts the first letter in a string to uppercase | |
| 333 | # ----------------------------------------------------------------------------- | |
| 334 | uc <- function( s ) { | |
| 335 | concat( toupper( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) ) | |
| 336 | } | |
| 337 | ||
| 338 | # ----------------------------------------------------------------------------- | |
| 339 | # Returns the number of days between the given dates. | |
| 340 | # ----------------------------------------------------------------------------- | |
| 341 | days <- function( d1, d2, format = "%Y-%m-%d" ) { | |
| 342 | dates = c( d1, d2 ) | |
| 343 | dt = strptime( dates, format = format ) | |
| 344 | as.integer( difftime( dates[2], dates[1], units = "days" ) ) | |
| 345 | } | |
| 346 | ||
| 347 | # ----------------------------------------------------------------------------- | |
| 348 | # Returns the number of years elapsed. | |
| 349 | # ----------------------------------------------------------------------------- | |
| 350 | years <- function( began, ended ) { | |
| 351 | began = when( anchor, began ) | |
| 352 | ended = when( anchor, ended ) | |
| 353 | ||
| 354 | # Swap the dates if the end date comes before the start date. | |
| 355 | if( as.integer( ended - began ) < 0 ) { | |
| 356 | tempd = began | |
| 357 | began = ended | |
| 358 | ended = tempd | |
| 359 | } | |
| 360 | ||
| 361 | # Calculate number of elapsed years. | |
| 362 | length( seq( from = began, to = ended, by = 'year' ) ) - 1 | |
| 363 | } | |
| 364 | ||
| 365 | # ----------------------------------------------------------------------------- | |
| 366 | # Full name of the month, starting with a capital letter. | |
| 367 | # ----------------------------------------------------------------------------- | |
| 368 | month <- function( n ) { | |
| 369 | # Faster than month.name[ x( n ) ] | |
| 370 | .subset( month.name, x( n ) ) | |
| 371 | } | |
| 372 | ||
| 373 | # ----------------------------------------------------------------------------- | |
| 374 | # ----------------------------------------------------------------------------- | |
| 375 | money <- function( n ) { | |
| 376 | commas( x( n ) ) | |
| 377 | } | |
| 378 | ||
| 379 | # ----------------------------------------------------------------------------- | |
| 380 | # ----------------------------------------------------------------------------- | |
| 381 | timeline <- function( n ) { | |
| 382 | concat( weekday( n ), ", ", annal( n ), " (", season( n ), ")" ) | |
| 383 | } | |
| 384 | ||
| 385 | # ----------------------------------------------------------------------------- | |
| 386 | # Rounds to the nearest base value (e.g., round to nearest 10). | |
| 387 | # | |
| 388 | # @param base The nearest value to round to. | |
| 389 | # ----------------------------------------------------------------------------- | |
| 390 | round.up <- function( n, base = 5 ) { | |
| 391 | base * round( x( n ) / base ) | |
| 392 | } | |
| 393 | ||
| 394 | # ----------------------------------------------------------------------------- | |
| 395 | # Computes linear distance between two points using Haversine formula. | |
| 396 | # Although Earth is an oblate spheroid, this will produce results close | |
| 397 | # enough for most purposes. | |
| 398 | # | |
| 399 | # @param lat1/lon1 The source latitude and longitude. | |
| 400 | # @param lat2/lon2 The destination latitude and longitude. | |
| 401 | # @param radius The radius of the sphere. | |
| 402 | # | |
| 403 | # @return The distance between the two coordinates in meters. | |
| 404 | # ----------------------------------------------------------------------------- | |
| 405 | haversine <- function( lat1, lon1, lat2, lon2, radius = 6371 ) { | |
| 406 | # Convert decimal degrees to radians | |
| 407 | lon1 = lon1 * pi / 180 | |
| 408 | lon2 = lon2 * pi / 180 | |
| 409 | lat1 = lat1 * pi / 180 | |
| 410 | lat2 = lat2 * pi / 180 | |
| 411 | ||
| 412 | # Haversine formula | |
| 413 | dlon = lon2 - lon1 | |
| 414 | dlat = lat2 - lat1 | |
| 415 | a = sin( dlat / 2 ) ** 2 + cos( lat1 ) * cos( lat2 ) * sin( dlon / 2 ) ** 2 | |
| 416 | c = 2 * atan2( sqrt( a ), sqrt( 1-a ) ) | |
| 417 | ||
| 418 | return( radius * c * 1000 ) | |
| 419 | } | |
| 420 | ||
| 1 | 421 |
| 1 | # ----------------------------------------------------------------------------- | |
| 2 | # Copyright 2020, White Magic Software, Ltd. | |
| 3 | # | |
| 4 | # Permission is hereby granted, free of charge, to any person obtaining | |
| 5 | # a copy of this software and associated documentation files (the | |
| 6 | # "Software"), to deal in the Software without restriction, including | |
| 7 | # without limitation the rights to use, copy, modify, merge, publish, | |
| 8 | # distribute, sublicense, and/or sell copies of the Software, and to | |
| 9 | # permit persons to whom the Software is furnished to do so, subject to | |
| 10 | # the following conditions: | |
| 11 | # | |
| 12 | # The above copyright notice and this permission notice shall be | |
| 13 | # included in all copies or substantial portions of the Software. | |
| 14 | # | |
| 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
| 16 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
| 17 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
| 18 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | |
| 19 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |
| 20 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
| 21 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| 22 | # ----------------------------------------------------------------------------- | |
| 23 | ||
| 24 | # ----------------------------------------------------------------------------- | |
| 25 | # Converts CSV to Markdown. | |
| 26 | # | |
| 27 | # Reads a CSV file and converts the contents to a Markdown table. The | |
| 28 | # file must be in the working directory as specified by setwd. | |
| 29 | # | |
| 30 | # @param f The filename to convert. | |
| 31 | # @param decimals Rounded decimal places (default 1). | |
| 32 | # @param totals Include total sums (default TRUE). | |
| 33 | # @param align Right-align numbers (default TRUE). | |
| 34 | # ----------------------------------------------------------------------------- | |
| 35 | csv2md <- function( f, decimals = 2, totals = T, align = T ) { | |
| 36 | # Read the CVS data from the file; ensure strings become characters. | |
| 37 | df <- read.table( f, sep=',', header=T, stringsAsFactors=F ) | |
| 38 | ||
| 39 | if( totals ) { | |
| 40 | # Determine what columns can be summed. | |
| 41 | number <- which( unlist( lapply( df, is.numeric ) ) ) | |
| 42 | ||
| 43 | # Use colSums when more than one summable column exists. | |
| 44 | if( length( number ) > 1 ) { | |
| 45 | f.sum <- colSums | |
| 46 | } | |
| 47 | else { | |
| 48 | f.sum <- sum | |
| 49 | } | |
| 50 | ||
| 51 | # Calculate the sum of all the summable columns and insert the | |
| 52 | # results back into the data frame. | |
| 53 | df[ (nrow( df ) + 1), number ] <- f.sum( df[, number], na.rm=TRUE ) | |
| 54 | ||
| 55 | # pluralise would be heavyweight here. | |
| 56 | if( length( number ) > 1 ) { | |
| 57 | t <- "**Totals**" | |
| 58 | } | |
| 59 | else { | |
| 60 | t <- "**Total**" | |
| 61 | } | |
| 62 | ||
| 63 | # Change the first column of the last line to "Total(s)". | |
| 64 | df[ nrow( df ), 1 ] <- t | |
| 65 | ||
| 66 | # Don't clutter the output with "NA" text. | |
| 67 | df[ is.na( df ) ] <- "" | |
| 68 | } | |
| 69 | ||
| 70 | if( align ) { | |
| 71 | is.char <- vapply( df, is.character, logical( 1 ) ) | |
| 72 | dashes <- paste( ifelse( is.char, ':---', '---:' ), collapse='|' ) | |
| 73 | } | |
| 74 | else { | |
| 75 | dashes <- paste( rep( '---', length( df ) ), collapse = '|' ) | |
| 76 | } | |
| 77 | ||
| 78 | # Create a Markdown version of the data frame. | |
| 79 | paste( | |
| 80 | paste( names( df ), collapse = '|'), '\n', | |
| 81 | dashes, '\n', | |
| 82 | paste( | |
| 83 | Reduce( function( x, y ) { | |
| 84 | paste( x, format( y, digits = decimals ), sep = '|' ) | |
| 85 | }, df | |
| 86 | ), | |
| 87 | collapse = '|\n', sep='' | |
| 88 | ) | |
| 89 | ) | |
| 90 | } | |
| 91 | ||
| 1 | 92 |
| 5 | 5 | A text editor that uses [interpolated strings](https://en.wikipedia.org/wiki/String_interpolation) to reference externally defined values. |
| 6 | 6 | |
| 7 | ## Requirements | |
| 7 | ## Download | |
| 8 | 8 | |
| 9 | Download and install the following software packages: | |
| 9 | Download one of the following editions: | |
| 10 | 10 | |
| 11 | * [OpenJDK 14](https://openjdk.java.net) | |
| 11 | * [Windows](https://gitreleases.dev/gh/DaveJarvis/scrivenvar/latest/scrivenvar.exe) | |
| 12 | * [Linux](https://gitreleases.dev/gh/DaveJarvis/scrivenvar/latest/scrivenvar.bin) | |
| 13 | * [Java Archive](https://gitreleases.dev/gh/DaveJarvis/scrivenvar/latest/scrivenvar.jar) | |
| 12 | 14 | |
| 13 | ## Quick Start | |
| 15 | ## Run | |
| 14 | 16 | |
| 15 | Complete the following steps to run the application: | |
| 17 | Note that the first time the application runs, it will unpack itself into a local directory. Subsequent starts will be faster. | |
| 16 | 18 | |
| 17 | 1. [Download](https://github.com/DaveJarvis/scrivenvar/releases) | |
| 18 | `scrivenvar.jar`. | |
| 19 | 1. Double-click `scrivenvar.jar` to start the application. | |
| 19 | ### Windows | |
| 20 | 20 | |
| 21 | ## Command Line Start | |
| 21 | On Windows, double-click the application to start. You will have to give the application permission to run. | |
| 22 | 22 | |
| 23 | If the quick start fails, run the application as follows: | |
| 23 | ### Linux | |
| 24 | 24 | |
| 25 | 1. Open a command prompt. | |
| 26 | 1. Change to the download directory containing the archive file. | |
| 27 | 1. Run: `java -jar scrivenvar.jar` | |
| 25 | On Linux, run `chmod +x scrivenvar.bin` then `./scrivenvar.bin`. | |
| 26 | ||
| 27 | ### Other | |
| 28 | ||
| 29 | On other platforms, download and install a full version of [OpenJDK 14](https://bell-sw.com/) that includes JavaFX module support, then run: | |
| 30 | ||
| 31 | ``` bash | |
| 32 | java -jar scrivenvar.jar | |
| 33 | ``` | |
| 28 | 34 | |
| 29 | 35 | ## Features |
| 30 | 36 | |
| 31 | * R integration | |
| 32 | * User-defined variables, interpolated | |
| 37 | * User-defined interpolated strings | |
| 33 | 38 | * Real-time preview with variable substitution |
| 34 | 39 | * Auto-complete variable names based on variable values |
| 35 | 40 | * XML document transformation using XSLT3 or older |
| 36 | 41 | * Platform independent (Windows, Linux, MacOS) |
| 42 | * R integration | |
| 37 | 43 | |
| 38 | 44 | ## Usage |
| 1 | import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform | |
| 2 | ||
| 3 | 1 | plugins { |
| 4 | 2 | id 'application' |
| ... | ||
| 25 | 23 | |
| 26 | 24 | if (project.hasProperty('targetOs')) { |
| 27 | if ("windows".equals(targetOs)) { | |
| 25 | if ("windows" == targetOs) { | |
| 28 | 26 | os = ["win"] |
| 29 | } | |
| 30 | else { | |
| 27 | } else { | |
| 31 | 28 | os = [targetOs] |
| 32 | 29 | } |
| ... | ||
| 92 | 89 | implementation 'org.apache.xmlgraphics:batik-util:1.13' |
| 93 | 90 | implementation 'org.apache.xmlgraphics:batik-xml:1.13' |
| 91 | ||
| 92 | // Spelling implementation | |
| 93 | implementation fileTree(include: ['**/*.jar'], dir: 'libs') | |
| 94 | 94 | |
| 95 | 95 | // Misc. |
| 96 | 96 | implementation 'org.ahocorasick:ahocorasick:0.4.0' |
| 97 | 97 | implementation 'org.apache.commons:commons-configuration2:2.7' |
| 98 | 98 | implementation 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3' |
| 99 | implementation 'javax.validation:validation-api:2.0.1.Final' | |
| 99 | 100 | |
| 100 | 101 | def fx = ['controls', 'graphics', 'fxml', 'swing'] |
| 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | |
| 2 | <svg | |
| 3 | xmlns:dc="http://purl.org/dc/elements/1.1/" | |
| 4 | xmlns:cc="http://creativecommons.org/ns#" | |
| 5 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | |
| 6 | xmlns:svg="http://www.w3.org/2000/svg" | |
| 7 | xmlns="http://www.w3.org/2000/svg" | |
| 8 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | |
| 9 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | |
| 10 | inkscape:export-ydpi="150.0097" | |
| 11 | inkscape:export-xdpi="150.0097" | |
| 12 | sodipodi:docname="architecture.svg" | |
| 13 | viewBox="0 0 764.4414 811.46748" | |
| 14 | height="811.46747" | |
| 15 | width="764.44141" | |
| 16 | id="svg4610" | |
| 17 | version="1.2" | |
| 18 | inkscape:version="1.0 (4035a4fb49, 2020-05-01)"> | |
| 19 | <metadata | |
| 20 | id="metadata4616"> | |
| 21 | <rdf:RDF> | |
| 22 | <cc:Work | |
| 23 | rdf:about=""> | |
| 24 | <dc:format>image/svg+xml</dc:format> | |
| 25 | <dc:type | |
| 26 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | |
| 27 | <dc:title /> | |
| 28 | </cc:Work> | |
| 29 | </rdf:RDF> | |
| 30 | </metadata> | |
| 31 | <defs | |
| 32 | id="defs4614"> | |
| 33 | <marker | |
| 34 | inkscape:stockid="Arrow1Mend" | |
| 35 | orient="auto" | |
| 36 | refY="0" | |
| 37 | refX="0" | |
| 38 | id="marker10933" | |
| 39 | style="overflow:visible" | |
| 40 | inkscape:isstock="true"> | |
| 41 | <path | |
| 42 | id="path10931" | |
| 43 | d="M 0,0 5,-5 -12.5,0 5,5 Z" | |
| 44 | style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1" | |
| 45 | transform="matrix(-0.4,0,0,-0.4,-4,0)" | |
| 46 | inkscape:connector-curvature="0" /> | |
| 47 | </marker> | |
| 48 | <marker | |
| 49 | inkscape:stockid="Arrow1Mend" | |
| 50 | orient="auto" | |
| 51 | refY="0" | |
| 52 | refX="0" | |
| 53 | id="marker9893" | |
| 54 | style="overflow:visible" | |
| 55 | inkscape:isstock="true"> | |
| 56 | <path | |
| 57 | id="path9891" | |
| 58 | d="M 0,0 5,-5 -12.5,0 5,5 Z" | |
| 59 | style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1" | |
| 60 | transform="matrix(-0.4,0,0,-0.4,-4,0)" | |
| 61 | inkscape:connector-curvature="0" /> | |
| 62 | </marker> | |
| 63 | <marker | |
| 64 | inkscape:collect="always" | |
| 65 | inkscape:isstock="true" | |
| 66 | style="overflow:visible" | |
| 67 | id="marker9767" | |
| 68 | refX="0" | |
| 69 | refY="0" | |
| 70 | orient="auto" | |
| 71 | inkscape:stockid="Arrow1Mend"> | |
| 72 | <path | |
| 73 | inkscape:connector-curvature="0" | |
| 74 | transform="matrix(-0.4,0,0,-0.4,-4,0)" | |
| 75 | style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1" | |
| 76 | d="M 0,0 5,-5 -12.5,0 5,5 Z" | |
| 77 | id="path9765" /> | |
| 78 | </marker> | |
| 79 | <marker | |
| 80 | inkscape:collect="always" | |
| 81 | inkscape:stockid="Arrow1Mend" | |
| 82 | orient="auto" | |
| 83 | refY="0" | |
| 84 | refX="0" | |
| 85 | id="marker9761" | |
| 86 | style="overflow:visible" | |
| 87 | inkscape:isstock="true"> | |
| 88 | <path | |
| 89 | id="path9759" | |
| 90 | d="M 0,0 5,-5 -12.5,0 5,5 Z" | |
| 91 | style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1" | |
| 92 | transform="matrix(-0.4,0,0,-0.4,-4,0)" | |
| 93 | inkscape:connector-curvature="0" /> | |
| 94 | </marker> | |
| 95 | <marker | |
| 96 | inkscape:isstock="true" | |
| 97 | style="overflow:visible" | |
| 98 | id="marker9750" | |
| 99 | refX="0" | |
| 100 | refY="0" | |
| 101 | orient="auto" | |
| 102 | inkscape:stockid="Arrow1Mend"> | |
| 103 | <path | |
| 104 | inkscape:connector-curvature="0" | |
| 105 | transform="matrix(-0.4,0,0,-0.4,-4,0)" | |
| 106 | style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1" | |
| 107 | d="M 0,0 5,-5 -12.5,0 5,5 Z" | |
| 108 | id="path9748" /> | |
| 109 | </marker> | |
| 110 | <marker | |
| 111 | inkscape:isstock="true" | |
| 112 | style="overflow:visible" | |
| 113 | id="marker9715" | |
| 114 | refX="0" | |
| 115 | refY="0" | |
| 116 | orient="auto" | |
| 117 | inkscape:stockid="Arrow1Mend"> | |
| 118 | <path | |
| 119 | inkscape:connector-curvature="0" | |
| 120 | transform="matrix(-0.4,0,0,-0.4,-4,0)" | |
| 121 | style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1" | |
| 122 | d="M 0,0 5,-5 -12.5,0 5,5 Z" | |
| 123 | id="path9713" /> | |
| 124 | </marker> | |
| 125 | <marker | |
| 126 | inkscape:collect="always" | |
| 127 | inkscape:stockid="Arrow1Mend" | |
| 128 | orient="auto" | |
| 129 | refY="0" | |
| 130 | refX="0" | |
| 131 | id="marker9685" | |
| 132 | style="overflow:visible" | |
| 133 | inkscape:isstock="true"> | |
| 134 | <path | |
| 135 | id="path9683" | |
| 136 | d="M 0,0 5,-5 -12.5,0 5,5 Z" | |
| 137 | style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1" | |
| 138 | transform="matrix(-0.4,0,0,-0.4,-4,0)" | |
| 139 | inkscape:connector-curvature="0" /> | |
| 140 | </marker> | |
| 141 | <marker | |
| 142 | inkscape:collect="always" | |
| 143 | inkscape:stockid="Arrow1Mend" | |
| 144 | orient="auto" | |
| 145 | refY="0" | |
| 146 | refX="0" | |
| 147 | id="marker9679" | |
| 148 | style="overflow:visible" | |
| 149 | inkscape:isstock="true"> | |
| 150 | <path | |
| 151 | id="path9677" | |
| 152 | d="M 0,0 5,-5 -12.5,0 5,5 Z" | |
| 153 | style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1" | |
| 154 | transform="matrix(-0.4,0,0,-0.4,-4,0)" | |
| 155 | inkscape:connector-curvature="0" /> | |
| 156 | </marker> | |
| 157 | <marker | |
| 158 | inkscape:collect="always" | |
| 159 | inkscape:isstock="true" | |
| 160 | style="overflow:visible" | |
| 161 | id="marker9640" | |
| 162 | refX="0" | |
| 163 | refY="0" | |
| 164 | orient="auto" | |
| 165 | inkscape:stockid="Arrow1Mend"> | |
| 166 | <path | |
| 167 | inkscape:connector-curvature="0" | |
| 168 | transform="matrix(-0.4,0,0,-0.4,-4,0)" | |
| 169 | style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1" | |
| 170 | d="M 0,0 5,-5 -12.5,0 5,5 Z" | |
| 171 | id="path9638" /> | |
| 172 | </marker> | |
| 173 | <marker | |
| 174 | inkscape:collect="always" | |
| 175 | inkscape:isstock="true" | |
| 176 | style="overflow:visible" | |
| 177 | id="marker9513" | |
| 178 | refX="0" | |
| 179 | refY="0" | |
| 180 | orient="auto" | |
| 181 | inkscape:stockid="Arrow1Mend"> | |
| 182 | <path | |
| 183 | inkscape:connector-curvature="0" | |
| 184 | transform="matrix(-0.4,0,0,-0.4,-4,0)" | |
| 185 | style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1" | |
| 186 | d="M 0,0 5,-5 -12.5,0 5,5 Z" | |
| 187 | id="path9511" /> | |
| 188 | </marker> | |
| 189 | <marker | |
| 190 | inkscape:stockid="Arrow1Mend" | |
| 191 | orient="auto" | |
| 192 | refY="0" | |
| 193 | refX="0" | |
| 194 | id="marker9509" | |
| 195 | style="overflow:visible" | |
| 196 | inkscape:isstock="true"> | |
| 197 | <path | |
| 198 | id="path9507" | |
| 199 | d="M 0,0 5,-5 -12.5,0 5,5 Z" | |
| 200 | style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1" | |
| 201 | transform="matrix(-0.4,0,0,-0.4,-4,0)" | |
| 202 | inkscape:connector-curvature="0" /> | |
| 203 | </marker> | |
| 204 | <marker | |
| 205 | inkscape:isstock="true" | |
| 206 | style="overflow:visible" | |
| 207 | id="marker9505" | |
| 208 | refX="0" | |
| 209 | refY="0" | |
| 210 | orient="auto" | |
| 211 | inkscape:stockid="Arrow1Mend"> | |
| 212 | <path | |
| 213 | inkscape:connector-curvature="0" | |
| 214 | transform="matrix(-0.4,0,0,-0.4,-4,0)" | |
| 215 | style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1" | |
| 216 | d="M 0,0 5,-5 -12.5,0 5,5 Z" | |
| 217 | id="path9503" /> | |
| 218 | </marker> | |
| 219 | <marker | |
| 220 | inkscape:collect="always" | |
| 221 | inkscape:stockid="Arrow1Mend" | |
| 222 | orient="auto" | |
| 223 | refY="0" | |
| 224 | refX="0" | |
| 225 | id="marker9479" | |
| 226 | style="overflow:visible" | |
| 227 | inkscape:isstock="true"> | |
| 228 | <path | |
| 229 | id="path9477" | |
| 230 | d="M 0,0 5,-5 -12.5,0 5,5 Z" | |
| 231 | style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1" | |
| 232 | transform="matrix(-0.4,0,0,-0.4,-4,0)" | |
| 233 | inkscape:connector-curvature="0" /> | |
| 234 | </marker> | |
| 235 | <clipPath | |
| 236 | id="ID000001"> | |
| 237 | <rect | |
| 238 | id="rect6" | |
| 239 | height="961.125" | |
| 240 | width="1381.6169" | |
| 241 | y="-43.688" | |
| 242 | x="-62.683998" /> | |
| 243 | </clipPath> | |
| 244 | <filter | |
| 245 | id="filter2842" | |
| 246 | inkscape:label="Drop Shadow" | |
| 247 | style="color-interpolation-filters:sRGB;"> | |
| 248 | <feFlood | |
| 249 | id="feFlood2832" | |
| 250 | result="flood" | |
| 251 | flood-color="rgb(0,0,0)" | |
| 252 | flood-opacity="0.498039" /> | |
| 253 | <feComposite | |
| 254 | id="feComposite2834" | |
| 255 | result="composite1" | |
| 256 | operator="in" | |
| 257 | in2="SourceGraphic" | |
| 258 | in="flood" /> | |
| 259 | <feGaussianBlur | |
| 260 | id="feGaussianBlur2836" | |
| 261 | result="blur" | |
| 262 | stdDeviation="2" | |
| 263 | in="composite1" /> | |
| 264 | <feOffset | |
| 265 | id="feOffset2838" | |
| 266 | result="offset" | |
| 267 | dy="3" | |
| 268 | dx="3" /> | |
| 269 | <feComposite | |
| 270 | id="feComposite2840" | |
| 271 | result="composite2" | |
| 272 | operator="over" | |
| 273 | in2="offset" | |
| 274 | in="SourceGraphic" /> | |
| 275 | </filter> | |
| 276 | <filter | |
| 277 | id="filter2854" | |
| 278 | inkscape:label="Drop Shadow" | |
| 279 | style="color-interpolation-filters:sRGB;"> | |
| 280 | <feFlood | |
| 281 | id="feFlood2844" | |
| 282 | result="flood" | |
| 283 | flood-color="rgb(0,0,0)" | |
| 284 | flood-opacity="0.498039" /> | |
| 285 | <feComposite | |
| 286 | id="feComposite2846" | |
| 287 | result="composite1" | |
| 288 | operator="in" | |
| 289 | in2="SourceGraphic" | |
| 290 | in="flood" /> | |
| 291 | <feGaussianBlur | |
| 292 | id="feGaussianBlur2848" | |
| 293 | result="blur" | |
| 294 | stdDeviation="2" | |
| 295 | in="composite1" /> | |
| 296 | <feOffset | |
| 297 | id="feOffset2850" | |
| 298 | result="offset" | |
| 299 | dy="3" | |
| 300 | dx="3" /> | |
| 301 | <feComposite | |
| 302 | id="feComposite2852" | |
| 303 | result="composite2" | |
| 304 | operator="over" | |
| 305 | in2="offset" | |
| 306 | in="SourceGraphic" /> | |
| 307 | </filter> | |
| 308 | <filter | |
| 309 | id="filter2866" | |
| 310 | inkscape:label="Drop Shadow" | |
| 311 | style="color-interpolation-filters:sRGB;"> | |
| 312 | <feFlood | |
| 313 | id="feFlood2856" | |
| 314 | result="flood" | |
| 315 | flood-color="rgb(0,0,0)" | |
| 316 | flood-opacity="0.498039" /> | |
| 317 | <feComposite | |
| 318 | id="feComposite2858" | |
| 319 | result="composite1" | |
| 320 | operator="in" | |
| 321 | in2="SourceGraphic" | |
| 322 | in="flood" /> | |
| 323 | <feGaussianBlur | |
| 324 | id="feGaussianBlur2860" | |
| 325 | result="blur" | |
| 326 | stdDeviation="2" | |
| 327 | in="composite1" /> | |
| 328 | <feOffset | |
| 329 | id="feOffset2862" | |
| 330 | result="offset" | |
| 331 | dy="3" | |
| 332 | dx="3" /> | |
| 333 | <feComposite | |
| 334 | id="feComposite2864" | |
| 335 | result="composite2" | |
| 336 | operator="over" | |
| 337 | in2="offset" | |
| 338 | in="SourceGraphic" /> | |
| 339 | </filter> | |
| 340 | <filter | |
| 341 | id="filter2878" | |
| 342 | inkscape:label="Drop Shadow" | |
| 343 | style="color-interpolation-filters:sRGB;"> | |
| 344 | <feFlood | |
| 345 | id="feFlood2868" | |
| 346 | result="flood" | |
| 347 | flood-color="rgb(0,0,0)" | |
| 348 | flood-opacity="0.498039" /> | |
| 349 | <feComposite | |
| 350 | id="feComposite2870" | |
| 351 | result="composite1" | |
| 352 | operator="in" | |
| 353 | in2="SourceGraphic" | |
| 354 | in="flood" /> | |
| 355 | <feGaussianBlur | |
| 356 | id="feGaussianBlur2872" | |
| 357 | result="blur" | |
| 358 | stdDeviation="2" | |
| 359 | in="composite1" /> | |
| 360 | <feOffset | |
| 361 | id="feOffset2874" | |
| 362 | result="offset" | |
| 363 | dy="3" | |
| 364 | dx="3" /> | |
| 365 | <feComposite | |
| 366 | id="feComposite2876" | |
| 367 | result="composite2" | |
| 368 | operator="over" | |
| 369 | in2="offset" | |
| 370 | in="SourceGraphic" /> | |
| 371 | </filter> | |
| 372 | <filter | |
| 373 | id="filter2890" | |
| 374 | inkscape:label="Drop Shadow" | |
| 375 | style="color-interpolation-filters:sRGB;"> | |
| 376 | <feFlood | |
| 377 | id="feFlood2880" | |
| 378 | result="flood" | |
| 379 | flood-color="rgb(0,0,0)" | |
| 380 | flood-opacity="0.498039" /> | |
| 381 | <feComposite | |
| 382 | id="feComposite2882" | |
| 383 | result="composite1" | |
| 384 | operator="in" | |
| 385 | in2="SourceGraphic" | |
| 386 | in="flood" /> | |
| 387 | <feGaussianBlur | |
| 388 | id="feGaussianBlur2884" | |
| 389 | result="blur" | |
| 390 | stdDeviation="2" | |
| 391 | in="composite1" /> | |
| 392 | <feOffset | |
| 393 | id="feOffset2886" | |
| 394 | result="offset" | |
| 395 | dy="3" | |
| 396 | dx="3" /> | |
| 397 | <feComposite | |
| 398 | id="feComposite2888" | |
| 399 | result="composite2" | |
| 400 | operator="over" | |
| 401 | in2="offset" | |
| 402 | in="SourceGraphic" /> | |
| 403 | </filter> | |
| 404 | <filter | |
| 405 | id="filter2902" | |
| 406 | inkscape:label="Drop Shadow" | |
| 407 | style="color-interpolation-filters:sRGB;"> | |
| 408 | <feFlood | |
| 409 | id="feFlood2892" | |
| 410 | result="flood" | |
| 411 | flood-color="rgb(0,0,0)" | |
| 412 | flood-opacity="0.498039" /> | |
| 413 | <feComposite | |
| 414 | id="feComposite2894" | |
| 415 | result="composite1" | |
| 416 | operator="in" | |
| 417 | in2="SourceGraphic" | |
| 418 | in="flood" /> | |
| 419 | <feGaussianBlur | |
| 420 | id="feGaussianBlur2896" | |
| 421 | result="blur" | |
| 422 | stdDeviation="2" | |
| 423 | in="composite1" /> | |
| 424 | <feOffset | |
| 425 | id="feOffset2898" | |
| 426 | result="offset" | |
| 427 | dy="3" | |
| 428 | dx="3" /> | |
| 429 | <feComposite | |
| 430 | id="feComposite2900" | |
| 431 | result="composite2" | |
| 432 | operator="over" | |
| 433 | in2="offset" | |
| 434 | in="SourceGraphic" /> | |
| 435 | </filter> | |
| 436 | <filter | |
| 437 | id="filter2914" | |
| 438 | inkscape:label="Drop Shadow" | |
| 439 | style="color-interpolation-filters:sRGB;"> | |
| 440 | <feFlood | |
| 441 | id="feFlood2904" | |
| 442 | result="flood" | |
| 443 | flood-color="rgb(0,0,0)" | |
| 444 | flood-opacity="0.498039" /> | |
| 445 | <feComposite | |
| 446 | id="feComposite2906" | |
| 447 | result="composite1" | |
| 448 | operator="in" | |
| 449 | in2="SourceGraphic" | |
| 450 | in="flood" /> | |
| 451 | <feGaussianBlur | |
| 452 | id="feGaussianBlur2908" | |
| 453 | result="blur" | |
| 454 | stdDeviation="2" | |
| 455 | in="composite1" /> | |
| 456 | <feOffset | |
| 457 | id="feOffset2910" | |
| 458 | result="offset" | |
| 459 | dy="3" | |
| 460 | dx="3" /> | |
| 461 | <feComposite | |
| 462 | id="feComposite2912" | |
| 463 | result="composite2" | |
| 464 | operator="over" | |
| 465 | in2="offset" | |
| 466 | in="SourceGraphic" /> | |
| 467 | </filter> | |
| 468 | <filter | |
| 469 | id="filter2926" | |
| 470 | inkscape:label="Drop Shadow" | |
| 471 | style="color-interpolation-filters:sRGB;"> | |
| 472 | <feFlood | |
| 473 | id="feFlood2916" | |
| 474 | result="flood" | |
| 475 | flood-color="rgb(0,0,0)" | |
| 476 | flood-opacity="0.498039" /> | |
| 477 | <feComposite | |
| 478 | id="feComposite2918" | |
| 479 | result="composite1" | |
| 480 | operator="in" | |
| 481 | in2="SourceGraphic" | |
| 482 | in="flood" /> | |
| 483 | <feGaussianBlur | |
| 484 | id="feGaussianBlur2920" | |
| 485 | result="blur" | |
| 486 | stdDeviation="2" | |
| 487 | in="composite1" /> | |
| 488 | <feOffset | |
| 489 | id="feOffset2922" | |
| 490 | result="offset" | |
| 491 | dy="3" | |
| 492 | dx="3" /> | |
| 493 | <feComposite | |
| 494 | id="feComposite2924" | |
| 495 | result="composite2" | |
| 496 | operator="over" | |
| 497 | in2="offset" | |
| 498 | in="SourceGraphic" /> | |
| 499 | </filter> | |
| 500 | <filter | |
| 501 | id="filter2938" | |
| 502 | inkscape:label="Drop Shadow" | |
| 503 | style="color-interpolation-filters:sRGB;"> | |
| 504 | <feFlood | |
| 505 | id="feFlood2928" | |
| 506 | result="flood" | |
| 507 | flood-color="rgb(0,0,0)" | |
| 508 | flood-opacity="0.498039" /> | |
| 509 | <feComposite | |
| 510 | id="feComposite2930" | |
| 511 | result="composite1" | |
| 512 | operator="in" | |
| 513 | in2="SourceGraphic" | |
| 514 | in="flood" /> | |
| 515 | <feGaussianBlur | |
| 516 | id="feGaussianBlur2932" | |
| 517 | result="blur" | |
| 518 | stdDeviation="2" | |
| 519 | in="composite1" /> | |
| 520 | <feOffset | |
| 521 | id="feOffset2934" | |
| 522 | result="offset" | |
| 523 | dy="3" | |
| 524 | dx="3" /> | |
| 525 | <feComposite | |
| 526 | id="feComposite2936" | |
| 527 | result="composite2" | |
| 528 | operator="over" | |
| 529 | in2="offset" | |
| 530 | in="SourceGraphic" /> | |
| 531 | </filter> | |
| 532 | <filter | |
| 533 | id="filter2950" | |
| 534 | inkscape:label="Drop Shadow" | |
| 535 | style="color-interpolation-filters:sRGB;"> | |
| 536 | <feFlood | |
| 537 | id="feFlood2940" | |
| 538 | result="flood" | |
| 539 | flood-color="rgb(0,0,0)" | |
| 540 | flood-opacity="0.498039" /> | |
| 541 | <feComposite | |
| 542 | id="feComposite2942" | |
| 543 | result="composite1" | |
| 544 | operator="in" | |
| 545 | in2="SourceGraphic" | |
| 546 | in="flood" /> | |
| 547 | <feGaussianBlur | |
| 548 | id="feGaussianBlur2944" | |
| 549 | result="blur" | |
| 550 | stdDeviation="2" | |
| 551 | in="composite1" /> | |
| 552 | <feOffset | |
| 553 | id="feOffset2946" | |
| 554 | result="offset" | |
| 555 | dy="3" | |
| 556 | dx="3" /> | |
| 557 | <feComposite | |
| 558 | id="feComposite2948" | |
| 559 | result="composite2" | |
| 560 | operator="over" | |
| 561 | in2="offset" | |
| 562 | in="SourceGraphic" /> | |
| 563 | </filter> | |
| 564 | <filter | |
| 565 | id="filter2962" | |
| 566 | inkscape:label="Drop Shadow" | |
| 567 | style="color-interpolation-filters:sRGB;"> | |
| 568 | <feFlood | |
| 569 | id="feFlood2952" | |
| 570 | result="flood" | |
| 571 | flood-color="rgb(0,0,0)" | |
| 572 | flood-opacity="0.498039" /> | |
| 573 | <feComposite | |
| 574 | id="feComposite2954" | |
| 575 | result="composite1" | |
| 576 | operator="in" | |
| 577 | in2="SourceGraphic" | |
| 578 | in="flood" /> | |
| 579 | <feGaussianBlur | |
| 580 | id="feGaussianBlur2956" | |
| 581 | result="blur" | |
| 582 | stdDeviation="2" | |
| 583 | in="composite1" /> | |
| 584 | <feOffset | |
| 585 | id="feOffset2958" | |
| 586 | result="offset" | |
| 587 | dy="3" | |
| 588 | dx="3" /> | |
| 589 | <feComposite | |
| 590 | id="feComposite2960" | |
| 591 | result="composite2" | |
| 592 | operator="over" | |
| 593 | in2="offset" | |
| 594 | in="SourceGraphic" /> | |
| 595 | </filter> | |
| 596 | <filter | |
| 597 | id="filter2974" | |
| 598 | inkscape:label="Drop Shadow" | |
| 599 | style="color-interpolation-filters:sRGB;"> | |
| 600 | <feFlood | |
| 601 | id="feFlood2964" | |
| 602 | result="flood" | |
| 603 | flood-color="rgb(0,0,0)" | |
| 604 | flood-opacity="0.498039" /> | |
| 605 | <feComposite | |
| 606 | id="feComposite2966" | |
| 607 | result="composite1" | |
| 608 | operator="in" | |
| 609 | in2="SourceGraphic" | |
| 610 | in="flood" /> | |
| 611 | <feGaussianBlur | |
| 612 | id="feGaussianBlur2968" | |
| 613 | result="blur" | |
| 614 | stdDeviation="2" | |
| 615 | in="composite1" /> | |
| 616 | <feOffset | |
| 617 | id="feOffset2970" | |
| 618 | result="offset" | |
| 619 | dy="3" | |
| 620 | dx="3" /> | |
| 621 | <feComposite | |
| 622 | id="feComposite2972" | |
| 623 | result="composite2" | |
| 624 | operator="over" | |
| 625 | in2="offset" | |
| 626 | in="SourceGraphic" /> | |
| 627 | </filter> | |
| 628 | <filter | |
| 629 | id="filter2986" | |
| 630 | inkscape:label="Drop Shadow" | |
| 631 | style="color-interpolation-filters:sRGB;"> | |
| 632 | <feFlood | |
| 633 | id="feFlood2976" | |
| 634 | result="flood" | |
| 635 | flood-color="rgb(0,0,0)" | |
| 636 | flood-opacity="0.498039" /> | |
| 637 | <feComposite | |
| 638 | id="feComposite2978" | |
| 639 | result="composite1" | |
| 640 | operator="in" | |
| 641 | in2="SourceGraphic" | |
| 642 | in="flood" /> | |
| 643 | <feGaussianBlur | |
| 644 | id="feGaussianBlur2980" | |
| 645 | result="blur" | |
| 646 | stdDeviation="2" | |
| 647 | in="composite1" /> | |
| 648 | <feOffset | |
| 649 | id="feOffset2982" | |
| 650 | result="offset" | |
| 651 | dy="3" | |
| 652 | dx="3" /> | |
| 653 | <feComposite | |
| 654 | id="feComposite2984" | |
| 655 | result="composite2" | |
| 656 | operator="over" | |
| 657 | in2="offset" | |
| 658 | in="SourceGraphic" /> | |
| 659 | </filter> | |
| 660 | </defs> | |
| 661 | <sodipodi:namedview | |
| 662 | inkscape:snap-text-baseline="false" | |
| 663 | inkscape:document-rotation="0" | |
| 664 | fit-margin-bottom="20" | |
| 665 | fit-margin-right="20" | |
| 666 | fit-margin-left="20" | |
| 667 | fit-margin-top="20" | |
| 668 | inkscape:current-layer="svg4610" | |
| 669 | inkscape:cy="370.55742" | |
| 670 | inkscape:cx="398.61418" | |
| 671 | inkscape:zoom="1.3753763" | |
| 672 | showgrid="false" | |
| 673 | id="namedview4612" | |
| 674 | inkscape:window-height="1280" | |
| 675 | inkscape:window-width="2055" | |
| 676 | inkscape:pageshadow="2" | |
| 677 | inkscape:pageopacity="1" | |
| 678 | guidetolerance="10" | |
| 679 | gridtolerance="10" | |
| 680 | objecttolerance="10" | |
| 681 | borderopacity="1" | |
| 682 | bordercolor="#666666" | |
| 683 | pagecolor="#ffffff" | |
| 684 | inkscape:window-x="215" | |
| 685 | inkscape:window-y="26" | |
| 686 | inkscape:window-maximized="0" /> | |
| 687 | <path | |
| 688 | sodipodi:nodetypes="ccssssc" | |
| 689 | inkscape:connector-curvature="0" | |
| 690 | style="fill:#333333;fill-opacity:0.0666667;fill-rule:nonzero;stroke:#df4d65;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | |
| 691 | d="M 53.35547,445.11522 V 790.96744 H 741.0332 c 1.6112,0 2.90821,-1.29701 2.90821,-2.9082 V 448.02342 c 0,-1.6112 -1.297,-2.9082 -2.90821,-2.9082 z" | |
| 692 | id="path9961" /> | |
| 693 | <path | |
| 694 | sodipodi:nodetypes="sssccssss" | |
| 695 | id="path9940" | |
| 696 | d="m 20.5,787.82486 c 0,0.87013 0.35019,1.65683 0.91797,2.22461 0.56778,0.56778 1.35253,0.91797 2.22265,0.91797 H 53.35547 V 445.11522 H 23.64062 c -0.87012,0 -1.65487,0.35019 -2.22265,0.91797 -0.56778,0.56778 -0.91797,1.35254 -0.91797,2.22266 z" | |
| 697 | style="fill:#df4d65;fill-opacity:1;fill-rule:nonzero;stroke:#df4d65;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | |
| 698 | inkscape:connector-curvature="0" /> | |
| 699 | <path | |
| 700 | sodipodi:nodetypes="sssccssss" | |
| 701 | id="path11125" | |
| 702 | d="m 20.5,423.31014 c 0,0.87013 0.35019,1.65683 0.91797,2.22461 0.56778,0.56778 1.354494,0.9764 2.22265,0.91797 H 53.35547 V 210.6005 H 23.64062 c -0.87012,0 -1.65487,0.3502 -2.22265,0.918 C 20.85019,212.08629 20.5,212.871 20.5,213.74109 Z" | |
| 703 | style="fill:#3e3e3e;fill-opacity:1;fill-rule:nonzero;stroke:#3e3e3e;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | |
| 704 | inkscape:connector-curvature="0" /> | |
| 705 | <path | |
| 706 | sodipodi:nodetypes="ccssssc" | |
| 707 | inkscape:connector-curvature="0" | |
| 708 | style="fill:#333333;fill-opacity:0.0666667;fill-rule:nonzero;stroke:#3e3e3e;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | |
| 709 | d="m 53.35547,210.6005 v 215.85222 h 687.67774 c 1.6112,0 2.9082,-1.29701 2.9082,-2.9082 V 213.5087 c 0,-1.6112 -1.29701,-2.90352 -2.9082,-2.9082 z" | |
| 710 | id="path11123" /> | |
| 711 | <path | |
| 712 | id="path6150" | |
| 713 | d="m 557.756,222.53493 c -0.87012,0 -1.65683,0.35019 -2.22461,0.91797 -0.56778,0.56778 -0.91797,1.35253 -0.91797,2.22265 v 29.71485 h 165.6211 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91797,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z" | |
| 714 | style="fill:#c53bd7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2926)" | |
| 715 | inkscape:connector-curvature="0" | |
| 716 | sodipodi:nodetypes="sssccssss" /> | |
| 717 | <path | |
| 718 | sodipodi:nodetypes="ccssssc" | |
| 719 | id="path6134" | |
| 720 | d="m 720.75716,255.39041 h -165.6211 v 152.63392 c 0,1.6112 1.29701,2.90821 2.90821,2.90821 h 159.80469 c 1.6112,0 2.9082,-1.29701 2.9082,-2.90821 z" | |
| 721 | style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2926)" | |
| 722 | inkscape:connector-curvature="0" /> | |
| 723 | <path | |
| 724 | id="path6082" | |
| 725 | d="m 317.13559,222.53494 c -0.87011,0 -1.65683,0.35019 -2.2246,0.91797 -0.56779,0.56778 -0.91798,1.35253 -0.91798,2.22265 v 29.71485 h 165.62111 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91798,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z" | |
| 726 | style="fill:#3dd092;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2938)" | |
| 727 | inkscape:connector-curvature="0" | |
| 728 | sodipodi:nodetypes="sssccssss" /> | |
| 729 | <path | |
| 730 | sodipodi:nodetypes="ccssssc" | |
| 731 | id="path6080" | |
| 732 | d="M 479.61412,255.39041 H 313.99301 v 152.63392 c 0,1.6112 1.29701,2.90821 2.90821,2.90821 h 159.80469 c 1.6112,0 2.90821,-1.29701 2.90821,-2.90821 z" | |
| 733 | style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2938)" | |
| 734 | inkscape:connector-curvature="0" /> | |
| 735 | <path | |
| 736 | id="path10980" | |
| 737 | d="M 53.35547,20.500012 V 188.35224 h 687.67774 c 1.6112,0 2.9082,-1.29701 2.9082,-2.9082 V 23.408212 c 0,-1.6112 -1.29701,-2.912886 -2.9082,-2.9082 z" | |
| 738 | style="fill:#333333;fill-opacity:0.0666667;fill-rule:nonzero;stroke:#3e3e3e;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | |
| 739 | inkscape:connector-curvature="0" | |
| 740 | sodipodi:nodetypes="ccssssc" /> | |
| 741 | <path | |
| 742 | inkscape:connector-curvature="0" | |
| 743 | style="fill:#3e3e3e;fill-opacity:1;fill-rule:nonzero;stroke:#3e3e3e;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | |
| 744 | d="m 20.5,185.20966 c 0,0.87013 0.35019,1.65683 0.91797,2.22461 0.56778,0.56778 1.35253,0.91797 2.22265,0.91797 H 53.35547 V 20.500012 H 23.64062 c -0.87012,0 -1.65487,0.350201 -2.22265,0.918 -0.56778,0.5678 -0.91797,1.3525 -0.91797,2.2226 z" | |
| 745 | id="path10982" | |
| 746 | sodipodi:nodetypes="sssccssss" /> | |
| 747 | <path | |
| 748 | sodipodi:nodetypes="sssccssss" | |
| 749 | inkscape:connector-curvature="0" | |
| 750 | style="fill:#c53bd7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2962)" | |
| 751 | d="m 557.75599,36.704447 c -0.87012,0 -1.65683,0.35019 -2.22461,0.91797 -0.56778,0.56778 -0.91797,1.35253 -0.91797,2.22265 v 29.71485 h 165.6211 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91797,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z" | |
| 752 | id="path4857" /> | |
| 753 | <path | |
| 754 | inkscape:connector-curvature="0" | |
| 755 | style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2962)" | |
| 756 | d="M 720.23451,69.559917 H 554.61341 V 169.2396 c 0,1.6112 1.29701,2.90821 2.90821,2.90821 h 159.80469 c 1.6112,0 2.9082,-1.29701 2.9082,-2.90821 z" | |
| 757 | id="path4853" /> | |
| 758 | <path | |
| 759 | sodipodi:nodetypes="sssccssss" | |
| 760 | inkscape:connector-curvature="0" | |
| 761 | style="fill:#3dd092;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2974)" | |
| 762 | d="m 317.13558,36.704447 c -0.87011,0 -1.65683,0.35019 -2.2246,0.91797 -0.56779,0.56778 -0.91798,1.35253 -0.91798,2.22265 v 29.71485 h 165.62111 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91798,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z" | |
| 763 | id="path5726" /> | |
| 764 | <path | |
| 765 | inkscape:connector-curvature="0" | |
| 766 | style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2974)" | |
| 767 | d="M 479.61411,69.559917 H 313.993 V 169.2396 c 0,1.6112 1.29701,2.90821 2.90821,2.90821 H 476.7059 c 1.6112,0 2.90821,-1.29701 2.90821,-2.90821 z" | |
| 768 | id="path5724" /> | |
| 769 | <path | |
| 770 | id="path4721" | |
| 771 | d="m 235.85308,44.704447 c 0.87012,0 1.65488,0.35019 2.22266,0.91797 0.56778,0.56778 0.91797,1.35253 0.91797,2.22265 v -0.23242 c 0,-1.6112 -1.297,-2.9082 -2.9082,-2.9082 z" | |
| 772 | style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | |
| 773 | inkscape:connector-curvature="0" /> | |
| 774 | <path | |
| 775 | sodipodi:nodetypes="sssccssss" | |
| 776 | id="path4719" | |
| 777 | d="m 76.515197,36.704447 c -0.870125,0 -1.656831,0.35019 -2.22461,0.91797 -0.567778,0.56778 -0.917968,1.35253 -0.917968,2.22265 v 29.71485 H 238.99371 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91797,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z" | |
| 778 | style="fill:#46c7f0;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2986)" | |
| 779 | inkscape:connector-curvature="0" /> | |
| 780 | <path | |
| 781 | id="path4723" | |
| 782 | d="M 238.99372,69.559917 H 73.372613 V 169.2396 c 0,1.6112 1.29701,2.90821 2.90821,2.90821 H 236.08552 c 1.6112,0 2.9082,-1.29701 2.9082,-2.90821 z" | |
| 783 | style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2986)" | |
| 784 | inkscape:connector-curvature="0" /> | |
| 785 | <path | |
| 786 | id="rect4622" | |
| 787 | d="m 76.280822,44.704447 c -1.611195,0 -2.908203,1.297 -2.908203,2.9082 v 0.23242 c 0,-0.87012 0.35019,-1.65487 0.917968,-2.22265 0.567779,-0.56778 1.354485,-0.91797 2.22461,-0.91797 z" | |
| 788 | style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | |
| 789 | inkscape:connector-curvature="0" /> | |
| 790 | <path | |
| 791 | sodipodi:nodetypes="cc" | |
| 792 | inkscape:connector-curvature="0" | |
| 793 | id="path9889" | |
| 794 | d="m 397.61301,500.62068 -0.50618,32.59418" | |
| 795 | style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:1.9694221;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9893)" /> | |
| 796 | <path | |
| 797 | style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9715)" | |
| 798 | d="m 554.61351,648.83688 -69.6817,47.69253" | |
| 799 | id="path9711" | |
| 800 | inkscape:connector-curvature="0" | |
| 801 | sodipodi:nodetypes="cc" /> | |
| 802 | <path | |
| 803 | sodipodi:nodetypes="cc" | |
| 804 | inkscape:connector-curvature="0" | |
| 805 | id="path9675" | |
| 806 | d="M 554.61351,567.95047 484.93181,615.643" | |
| 807 | style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9679)" /> | |
| 808 | <rect | |
| 809 | ry="3.9839513" | |
| 810 | rx="3.9205718" | |
| 811 | y="537.09552" | |
| 812 | x="554.61353" | |
| 813 | height="32.855" | |
| 814 | width="165.621" | |
| 815 | id="rect9618" | |
| 816 | style="opacity:1;fill:#c53bd7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2842)" /> | |
| 817 | <rect | |
| 818 | ry="3.9839513" | |
| 819 | rx="3.9205718" | |
| 820 | y="537.09552" | |
| 821 | x="73.372665" | |
| 822 | height="32.855" | |
| 823 | width="165.621" | |
| 824 | id="rect9614" | |
| 825 | style="opacity:1;fill:#46c7f0;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2914)" /> | |
| 826 | <path | |
| 827 | inkscape:connector-curvature="0" | |
| 828 | style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | |
| 829 | d="m 235.85308,545.09525 c 0.87012,0 1.65488,0.35019 2.22266,0.91797 0.56778,0.56778 0.91797,1.35253 0.91797,2.22265 v -0.23242 c 0,-1.6112 -1.297,-2.9082 -2.9082,-2.9082 z" | |
| 830 | id="path9323" /> | |
| 831 | <path | |
| 832 | inkscape:connector-curvature="0" | |
| 833 | style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | |
| 834 | d="m 76.280823,545.09525 c -1.611195,0 -2.908203,1.297 -2.908203,2.9082 v 0.23242 c 0,-0.87012 0.35019,-1.65487 0.917968,-2.22265 0.567779,-0.56778 1.354485,-0.91797 2.22461,-0.91797 z" | |
| 835 | id="path9327" /> | |
| 836 | <rect | |
| 837 | style="opacity:1;fill:#3dd092;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2902)" | |
| 838 | id="rect9616" | |
| 839 | width="165.621" | |
| 840 | height="32.855" | |
| 841 | x="313.99307" | |
| 842 | y="537.09552" | |
| 843 | rx="3.9205718" | |
| 844 | ry="3.9839513" /> | |
| 845 | <path | |
| 846 | sodipodi:nodetypes="cc" | |
| 847 | inkscape:connector-curvature="0" | |
| 848 | id="path9491" | |
| 849 | d="m 240.99257,554.11276 65.23376,-1.01307" | |
| 850 | style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9513)" /> | |
| 851 | <path | |
| 852 | style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9640)" | |
| 853 | d="m 481.61298,554.11276 65.23376,-1.01307" | |
| 854 | id="path9501" | |
| 855 | inkscape:connector-curvature="0" | |
| 856 | sodipodi:nodetypes="cc" /> | |
| 857 | <rect | |
| 858 | ry="3.9839513" | |
| 859 | rx="3.9205718" | |
| 860 | y="617.79578" | |
| 861 | x="313.99307" | |
| 862 | height="32.855" | |
| 863 | width="165.621" | |
| 864 | id="rect9620" | |
| 865 | style="opacity:1;fill:#3dd092;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2890)" /> | |
| 866 | <path | |
| 867 | sodipodi:nodetypes="cc" | |
| 868 | inkscape:connector-curvature="0" | |
| 869 | id="path9681" | |
| 870 | d="m 481.61298,634.81299 65.23376,-1.01307" | |
| 871 | style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9685)" /> | |
| 872 | <rect | |
| 873 | style="opacity:1;fill:#c53bd7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2854)" | |
| 874 | id="rect9687" | |
| 875 | width="165.621" | |
| 876 | height="32.855" | |
| 877 | x="554.61353" | |
| 878 | y="617.79578" | |
| 879 | rx="3.9205718" | |
| 880 | ry="3.9839513" /> | |
| 881 | <path | |
| 882 | style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9750)" | |
| 883 | d="m 481.61298,715.51321 65.23376,-1.01307" | |
| 884 | id="path9734" | |
| 885 | inkscape:connector-curvature="0" | |
| 886 | sodipodi:nodetypes="cc" /> | |
| 887 | <rect | |
| 888 | ry="3.9839513" | |
| 889 | rx="3.9205718" | |
| 890 | y="698.49591" | |
| 891 | x="554.61353" | |
| 892 | height="32.855" | |
| 893 | width="165.621" | |
| 894 | id="rect9736" | |
| 895 | style="opacity:1;fill:#c53bd7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2866)" /> | |
| 896 | <path | |
| 897 | id="path9830" | |
| 898 | d="m 356.40451,489.45323 c -0.80426,0 -1.45167,0.64741 -1.45167,1.45166 v 0.11602 c 0,-0.43433 0.1748,-0.82605 0.45822,-1.10946 0.28341,-0.28342 0.6761,-0.45822 1.11043,-0.45822 z" | |
| 899 | style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.10902636;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | |
| 900 | inkscape:connector-curvature="0" /> | |
| 901 | <rect | |
| 902 | style="opacity:1;fill:#ffb73a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.9391377;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | |
| 903 | id="rect9826" | |
| 904 | width="120.98324" | |
| 905 | height="24" | |
| 906 | x="336.82672" | |
| 907 | y="477.86002" | |
| 908 | rx="2.8639088" | |
| 909 | ry="2.9102066" /> | |
| 910 | <path | |
| 911 | id="path10514" | |
| 912 | d="m 235.85301,637.23875 c 0.87012,0 1.65488,0.35019 2.22266,0.91797 0.56778,0.56778 0.91797,1.35253 0.91797,2.22265 v -0.23242 c 0,-1.6112 -1.297,-2.9082 -2.9082,-2.9082 z" | |
| 913 | style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | |
| 914 | inkscape:connector-curvature="0" /> | |
| 915 | <rect | |
| 916 | style="opacity:1;fill:#3dd092;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2878)" | |
| 917 | id="rect9717" | |
| 918 | width="165.621" | |
| 919 | height="32.855" | |
| 920 | x="313.99307" | |
| 921 | y="698.49591" | |
| 922 | rx="3.9205718" | |
| 923 | ry="3.9839513" /> | |
| 924 | <path | |
| 925 | id="path10537" | |
| 926 | d="M 238.99366,636.97465 H 73.372671 V 729.175 c 0,1.2055 0.970418,2.17592 2.175911,2.17592 H 236.81776 c 1.20549,0 2.1759,-0.97042 2.1759,-2.17592 z" | |
| 927 | style="opacity:1;fill:#333333;fill-opacity:0.93333333;fill-rule:nonzero;stroke:none;stroke-width:0.16342013;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | |
| 928 | inkscape:connector-curvature="0" | |
| 929 | sodipodi:nodetypes="ccssssc" /> | |
| 930 | <path | |
| 931 | sodipodi:nodetypes="sssccssss" | |
| 932 | id="path10516" | |
| 933 | d="m 75.723937,612.39226 c -0.651025,0 -1.239637,0.26201 -1.664447,0.68682 -0.424811,0.42482 -0.686822,1.01196 -0.686822,1.66299 v 22.23258 H 238.99366 v -22.23258 c 0,-0.65103 -0.26201,-1.23817 -0.68682,-1.66299 -0.42481,-0.42481 -1.01197,-0.68682 -1.66299,-0.68682 z" | |
| 934 | style="opacity:1;fill:#ffb73a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.16342013;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | |
| 935 | inkscape:connector-curvature="0" /> | |
| 936 | <path | |
| 937 | style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker10933)" | |
| 938 | d="m 240.99257,715.51318 65.23376,-1.01307" | |
| 939 | id="path10929" | |
| 940 | inkscape:connector-curvature="0" | |
| 941 | sodipodi:nodetypes="cc" /> | |
| 942 | <path | |
| 943 | style="fill:#df4d65;fill-opacity:1;fill-rule:evenodd;stroke-width:0.05250736" | |
| 944 | d="m 399.47357,99.155037 -0.12716,0.0127 -0.12716,0.0141 -0.12574,0.0141 -0.12716,0.0155 -0.61883,0.967813 -0.29106,0.9325 -0.17943,0.91977 -0.1215,1.09073 -0.30943,0.0636 -0.308,0.0678 -0.308,0.0735 -0.30801,0.0763 -0.58069,-0.93108 -0.5609,-0.75023 -0.66687,-0.71349 -0.97912,-0.6033 -0.12009,0.0409 -0.1201,0.0424 -0.12009,0.0424 -0.12009,0.0424 -0.38854,1.08085 -0.0763,0.97487 0.0297,0.93673 0.12575,1.08932 -0.28823,0.1314 -0.28681,0.13563 -0.28398,0.13987 -0.28117,0.14271 -0.77425,-0.77849 -0.7149,-0.60612 -0.80816,-0.54819 -1.0879,-0.37017 -0.10879,0.0678 -0.10738,0.0679 -0.10738,0.0678 -0.10738,0.0678 -0.13846,1.14018 0.14129,0.9664 0.23735,0.90706 0.36594,1.03563 -0.25149,0.19215 -0.24865,0.19497 -0.24585,0.19922 -0.24442,0.20204 -0.92825,-0.58634 -0.83077,-0.43234 -0.90988,-0.35463 -1.14301,-0.11868 -0.0904,0.0904 -0.0904,0.089 -0.089,0.0904 -0.0904,0.0904 0.11868,1.14301 0.35462,0.90988 0.43234,0.83218 0.58634,0.92684 -0.20204,0.24442 -0.19921,0.24584 -0.19498,0.24867 -0.19215,0.25148 -1.03563,-0.36593 -0.90705,-0.23736 -0.96641,-0.14128 -1.14018,0.13845 -0.0678,0.10738 -0.0678,0.10738 -0.0678,0.10738 -0.0678,0.10879 0.37016,1.0879 0.5482,0.80816 0.60612,0.71491 0.77848,0.77425 -0.1427,0.28117 -0.13987,0.28398 -0.13564,0.28681 -0.13139,0.28823 -1.09073,-0.12575 -0.93532,-0.0297 -0.97487,0.0763 -1.08084,0.38854 -0.0424,0.12009 -0.0424,0.12009 -0.0424,0.1201 -0.0409,0.12009 0.6033,0.97912 0.7135,0.66686 0.75023,0.56091 0.93107,0.58069 -0.0763,0.30801 -0.0735,0.308 -0.0678,0.308 -0.0636,0.30942 -1.09073,0.1215 -0.91977,0.17944 -0.9325,0.29105 -0.96781,0.61883 -0.0156,0.12717 -0.0141,0.12574 -0.0141,0.12716 -0.0127,0.12716 0.80533,0.81804 0.84348,0.49168 0.85619,0.38006 1.03704,0.36028 -0.006,0.31648 -0.003,0.31648 0.003,0.31648 0.006,0.31648 -1.03704,0.35887 -0.85619,0.38006 -0.84348,0.49168 -0.80533,0.81946 0.0127,0.12716 0.0141,0.12574 0.0141,0.12716 0.0156,0.12574 0.96781,0.62026 0.9325,0.29104 0.91977,0.17944 1.09073,0.12009 0.0636,0.30942 0.0678,0.30941 0.0735,0.30801 0.0763,0.30659 -0.93107,0.5821 -0.75023,0.56091 -0.7135,0.66687 -0.6033,0.97771 0.0409,0.12008 0.0424,0.12151 0.0424,0.1201 0.0424,0.11868 1.08084,0.38995 0.97487,0.0763 0.93532,-0.0297 1.09073,-0.12574 0.13139,0.28822 0.13564,0.2854 0.13987,0.28399 0.1427,0.28257 -0.77848,0.77425 -0.60612,0.7135 -0.5482,0.80957 -0.37016,1.08791 0.0678,0.10737 0.0678,0.10879 0.0678,0.10738 0.0678,0.10738 1.14018,0.13846 0.96641,-0.1427 0.90705,-0.23736 1.03563,-0.36452 0.19215,0.25149 0.19498,0.24866 0.19921,0.24584 0.20204,0.24302 -0.58634,0.92825 -0.43234,0.83076 -0.35462,0.9113 -0.11868,1.14159 0.0904,0.0918 0.089,0.089 0.0904,0.0904 0.0904,0.089 1.14301,-0.11868 0.90988,-0.35321 0.83077,-0.43375 0.92825,-0.58493 0.24442,0.20204 0.24585,0.19921 0.24865,0.19497 0.25149,0.19216 -0.36594,1.03563 -0.23735,0.90564 -0.14129,0.9664 0.13846,1.14018 0.10738,0.0692 0.10738,0.0678 0.10738,0.0678 0.10879,0.0664 1.0879,-0.37017 0.80816,-0.54677 0.7149,-0.60754 0.77425,-0.77708 0.28117,0.14271 0.28398,0.13987 0.28681,0.13422 0.28823,0.13139 -0.12575,1.09074 -0.0297,0.93673 0.0763,0.97346 0.38854,1.08084 0.12009,0.0438 0.12009,0.0424 0.1201,0.041 0.12009,0.0409 0.97912,-0.60188 0.66687,-0.71349 0.5609,-0.75165 0.58069,-0.93108 0.30801,0.0777 0.308,0.072 0.308,0.0692 0.30943,0.0636 0.1215,1.09073 0.17943,0.91978 0.29106,0.93249 0.61883,0.9664 0.12716,0.0156 0.12574,0.0141 0.12716,0.0141 0.12716,0.0141 0.81806,-0.80533 0.49167,-0.8449 0.38006,-0.85619 0.36028,-1.03704 0.31648,0.007 0.31648,0.003 0.31649,-0.003 0.31648,-0.007 0.36028,1.03704 0.37865,0.85619 0.49167,0.8449 0.81947,0.80533 0.12715,-0.0141 0.12574,-0.0141 0.12717,-0.0141 0.12574,-0.0156 0.62025,-0.9664 0.29104,-0.93249 0.17944,-0.91978 0.12009,-1.09073 0.30942,-0.0636 0.30942,-0.0692 0.30799,-0.072 0.3066,-0.0777 0.58211,0.93108 0.5609,0.75165 0.66687,0.71349 0.97771,0.60188 0.12009,-0.0409 0.1215,-0.041 0.1201,-0.0424 0.11868,-0.0438 0.38995,-1.08084 0.0763,-0.97346 -0.0297,-0.93673 -0.12574,-1.09074 0.28822,-0.13139 0.2854,-0.13422 0.28398,-0.13987 0.28258,-0.14271 0.77424,0.77708 0.7135,0.60754 0.80957,0.54677 1.08791,0.37017 0.10737,-0.0664 0.10879,-0.0678 0.10738,-0.0678 0.10738,-0.0692 0.13847,-1.14018 -0.14271,-0.9664 -0.23737,-0.90564 -0.36452,-1.03563 0.25149,-0.19216 0.24866,-0.19497 0.24585,-0.19921 0.24301,-0.20204 0.92825,0.58493 0.83077,0.43375 0.91129,0.35321 1.1416,0.11868 0.0904,-0.089 0.0904,-0.0904 0.0904,-0.089 0.089,-0.0918 -0.11868,-1.14159 -0.35321,-0.9113 -0.43375,-0.83076 -0.58492,-0.92825 0.20203,-0.24302 0.19921,-0.24584 0.19498,-0.24866 0.19215,-0.25149 1.03563,0.36452 0.90564,0.23736 0.9664,0.1427 1.14018,-0.13846 0.0692,-0.10738 0.0678,-0.10738 0.0678,-0.10879 0.0664,-0.10737 -0.37017,-1.08791 -0.54677,-0.80957 -0.60754,-0.7135 -0.77706,-0.77425 0.1427,-0.28257 0.13986,-0.28399 0.13423,-0.2854 0.13139,-0.28822 1.09073,0.12574 0.93674,0.0297 0.97345,-0.0763 1.08085,-0.38995 0.0438,-0.11868 0.0424,-0.1201 0.0409,-0.12151 0.041,-0.12008 -0.6019,-0.97771 -0.71349,-0.66687 -0.75164,-0.56091 -0.93108,-0.5821 0.0777,-0.30659 0.072,-0.30801 0.0692,-0.30941 0.0636,-0.30942 1.09073,-0.12009 0.91978,-0.17944 0.93249,-0.29104 0.9664,-0.62026 0.0155,-0.12574 0.0141,-0.12716 0.0141,-0.12574 0.0141,-0.12716 -0.80533,-0.81946 -0.8449,-0.49168 -0.85619,-0.38006 -1.03704,-0.35887 0.007,-0.31648 0.003,-0.31648 -0.003,-0.31648 -0.007,-0.31648 1.03704,-0.36028 0.85619,-0.38006 0.8449,-0.49168 0.80533,-0.81804 -0.0141,-0.12716 -0.0141,-0.12716 -0.0141,-0.12574 -0.0155,-0.12717 -0.9664,-0.61883 -0.93249,-0.29105 -0.91978,-0.17944 -1.09073,-0.1215 -0.0636,-0.30942 -0.0692,-0.308 -0.072,-0.308 -0.0777,-0.30801 0.93108,-0.58069 0.75164,-0.56091 0.71349,-0.66686 0.6019,-0.97912 -0.041,-0.12009 -0.0409,-0.1201 -0.0424,-0.12009 -0.0438,-0.12009 -1.08085,-0.38854 -0.97345,-0.0763 -0.93674,0.0297 -1.09073,0.12575 -0.13139,-0.28823 -0.13423,-0.28681 -0.13986,-0.28398 -0.1427,-0.28117 0.77706,-0.77425 0.60754,-0.71491 0.54677,-0.80816 0.37017,-1.0879 -0.0664,-0.10879 -0.0678,-0.10738 -0.0678,-0.10738 -0.0692,-0.10738 -1.14018,-0.13845 -0.9664,0.14128 -0.90564,0.23736 -1.03563,0.36593 -0.19215,-0.25148 -0.19498,-0.24867 -0.19921,-0.24584 -0.20203,-0.24442 0.58492,-0.92684 0.43375,-0.83218 0.35321,-0.90988 0.11868,-1.14301 -0.089,-0.0904 -0.0904,-0.0904 -0.0904,-0.089 -0.0904,-0.0904 -1.1416,0.11868 -0.91129,0.35463 -0.83077,0.43234 -0.92825,0.58634 -0.24301,-0.20204 -0.24585,-0.19922 -0.24866,-0.19497 -0.25149,-0.19215 0.36452,-1.03563 0.23737,-0.90706 0.14271,-0.9664 -0.13847,-1.14018 -0.10738,-0.0678 -0.10738,-0.0678 -0.10879,-0.0679 -0.10737,-0.0678 -1.08791,0.37017 -0.80957,0.54819 -0.7135,0.60612 -0.77424,0.77849 -0.28258,-0.14271 -0.28398,-0.13987 -0.2854,-0.13563 -0.28822,-0.1314 0.12574,-1.08932 0.0297,-0.93673 -0.0763,-0.97487 -0.38995,-1.08085 -0.11868,-0.0424 -0.1201,-0.0424 -0.1215,-0.0424 -0.12009,-0.0409 -0.97771,0.6033 -0.66687,0.71349 -0.5609,0.75023 -0.58211,0.93108 -0.3066,-0.0763 -0.30799,-0.0735 -0.30942,-0.0678 -0.30942,-0.0636 -0.12009,-1.09073 -0.17944,-0.91977 -0.29104,-0.9325 -0.62025,-0.967813 -0.12574,-0.0155 -0.12717,-0.0141 -0.12574,-0.0141 -0.12715,-0.0127 -0.81947,0.80533 -0.49167,0.843483 -0.37865,0.8562 -0.36028,1.03704 -0.31648,-0.006 -0.31649,-0.003 -0.31648,0.003 -0.31648,0.006 -0.36028,-1.03704 -0.38006,-0.8562 -0.49167,-0.843483 z m 2.68302,20.688573 a 5.3990039,5.3990039 0 0 1 5.39856,5.39997 5.3990039,5.3990039 0 0 1 -5.39856,5.39855 5.3990039,5.3990039 0 0 1 -5.39996,-5.39855 5.3990039,5.3990039 0 0 1 5.39996,-5.39997 z" | |
| 945 | id="path5693" | |
| 946 | inkscape:connector-curvature="0" /> | |
| 947 | <path | |
| 948 | inkscape:connector-curvature="0" | |
| 949 | d="m 380.9529,101.31918 a 4.37599,4.37599 0 0 1 -4.37599,4.37599 4.37599,4.37599 0 0 1 -4.37599,-4.37599 4.37599,4.37599 0 0 1 4.37599,-4.375983 4.37599,4.37599 0 0 1 4.37599,4.375983 z m 4.63493,-1.27213 c -0.32212,-0.118873 -0.95326,0.0926 -0.92258,-0.401293 -0.13877,-0.39635 -0.21401,-0.74537 0.27363,-0.88946 0.78055,-0.47633 1.45123,-1.16128 1.74461,-2.04171 0.15411,-0.39145 -0.3432,-0.48754 -0.63657,-0.53536 -0.91614,-0.25589 -1.86519,0.0578 -2.73328,0.35995 -0.11023,-0.31345 -0.69059,-0.56868 -0.47901,-0.88657 0.56405,-0.84324 0.99162,-1.8335 0.85012,-2.86709 -0.031,-0.41955 -0.52073,-0.29038 -0.8058,-0.20618 -0.93646,0.16692 -1.65537,0.86143 -2.30642,1.51029 -0.23528,-0.23464 -0.86896,-0.21274 -0.81624,-0.59097 0.14241,-1.00446 0.0978,-2.08217 -0.47806,-2.95201 -0.20995,-0.36459 -0.59514,-0.0357 -0.81544,0.16385 -0.7713,0.55671 -1.11768,1.49434 -1.42274,2.36143 -0.3138,-0.10927 -0.87519,0.18536 -0.99181,-0.17826 -0.30757,-0.96675 -0.81528,-1.91841 -1.71153,-2.45229 -0.34734,-0.23737 -0.55176,0.22606 -0.66364,0.50149 -0.45331,0.83622 -0.35865,1.83127 -0.25723,2.74482 -0.33012,0.0378 -0.70817,0.54672 -0.97098,0.26977 -0.69651,-0.73763 -1.56687,-1.37476 -2.60601,-1.46682 -0.41593,-0.0631 -0.39904,0.443 -0.38034,0.73971 -0.0456,0.95009 0.47143,1.80554 0.95918,2.58463 -0.28104,0.17725 -0.40076,0.79984 -0.75777,0.6643 -0.94759,-0.36236 -2.00818,-0.55864 -2.98437,-0.1908 -0.40215,0.12357 -0.16723,0.57227 -0.0217,0.83147 0.37114,0.8758 1.2081,1.42221 1.98561,1.91248 -0.17632,0.28163 -0.0141,0.89453 -0.39449,0.92732 -1.01097,0.0847 -2.05173,0.367983 -2.77166,1.122963 -0.3087,0.2858 0.0976,0.58816 0.34122,0.75853 0.71439,0.62804 1.70558,0.75716 2.6188,0.8616 -0.0367,0.33025 0.37548,0.81204 0.0469,1.00665 -0.87415,0.51488 -1.68886,1.22174 -2.00994,2.2143 -0.15411,0.39144 0.3432,0.48753 0.63657,0.53535 0.91612,0.25589 1.86516,-0.0578 2.73323,-0.35995 0.11029,0.31341 0.69063,0.56869 0.47905,0.88657 -0.56412,0.8432 -0.99155,1.83352 -0.85015,2.86709 0.031,0.41952 0.52075,0.2904 0.80584,0.20618 0.93644,-0.16692 1.65537,-0.86139 2.30637,-1.51029 0.2353,0.23464 0.86901,0.21272 0.81629,0.59098 -0.14241,1.00446 -0.0978,2.08215 0.47802,2.95202 0.20997,0.36455 0.59517,0.0357 0.81548,-0.16387 0.77125,-0.55674 1.11768,-1.49435 1.42274,-2.36142 0.31379,0.10926 0.8752,-0.18537 0.99181,0.17824 0.30754,0.96678 0.81527,1.91842 1.71153,2.45229 0.34733,0.23738 0.55172,-0.22608 0.66362,-0.50146 0.45335,-0.83621 0.35866,-1.83128 0.25725,-2.74484 0.33011,-0.0378 0.70812,-0.54672 0.97093,-0.26977 0.69656,0.7376 1.5669,1.37477 2.60606,1.46683 0.41593,0.0632 0.39897,-0.44304 0.38032,-0.73972 0.0457,-0.95011 -0.4715,-1.8055 -0.95916,-2.58463 0.28105,-0.17722 0.40074,-0.79983 0.75772,-0.6643 0.94761,0.36234 2.00821,0.55865 2.98442,0.1908 0.40215,-0.12357 0.16723,-0.57228 0.0217,-0.83146 -0.37116,-0.87579 -1.20814,-1.42218 -1.98561,-1.91249 0.17632,-0.28163 0.0141,-0.89453 0.39449,-0.92732 1.01097,-0.0847 2.05173,-0.36799 2.77166,-1.12295 0.3087,-0.28581 -0.0976,-0.58817 -0.34122,-0.75854 -0.47483,-0.43652 -1.13407,-0.61787 -1.75144,-0.75008 z" | |
| 950 | style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke-width:0.04014921" | |
| 951 | id="path5687" /> | |
| 952 | <path | |
| 953 | style="fill:#05556e;fill-opacity:1;stroke-width:0.11881336" | |
| 954 | id="path4816" | |
| 955 | d="m 619.92265,90.37586 h 2.72717 v 2.71445 h 2.51374 v -2.71445 h 2.72716 v 8.21477 h -2.72716 v -2.73825 h -2.49003 v 2.73825 h -2.75088 m 11.57268,-5.47651 h -2.40702 v -2.73826 h 7.55307 v 2.73826 h -2.41888 v 5.47651 h -2.72717 m 6.34363,-8.21477 h 2.8576 l 1.75487,2.89303 1.75487,-2.89303 h 2.8576 v 8.21477 h -2.72717 v -4.07167 l -1.90901,2.95256 -1.90902,-2.95256 v 4.07167 h -2.67974 m 10.57667,-8.21477 h 2.72717 v 5.50033 h 3.86546 v 2.71444 h -6.59263" | |
| 956 | inkscape:connector-curvature="0" /> | |
| 957 | <path | |
| 958 | id="path4818" | |
| 959 | d="m 619.82779,146.45062 -3.91289,-44.09786 h 43.01811 l -3.91289,44.07405 -17.63174,4.90505" | |
| 960 | inkscape:connector-curvature="0" | |
| 961 | style="fill:#e44d26;stroke-width:0.11881336" /> | |
| 962 | <path | |
| 963 | id="path4820" | |
| 964 | d="m 637.42396,147.58164 v -41.60962 h 17.5843 l -3.3556,37.62129" | |
| 965 | inkscape:connector-curvature="0" | |
| 966 | style="fill:#f16529;stroke-width:0.11881336" /> | |
| 967 | <path | |
| 968 | id="path4822" | |
| 969 | d="m 623.90669,111.3652 h 13.51727 v 5.40508 h -7.61236 l 0.498,5.53605 h 7.11436 v 5.39318 h -12.04697 m 0.23714,2.71444 h 5.40691 l 0.37943,4.32169 6.02349,1.61914 v 5.64319 L 626.373,138.90255" | |
| 970 | inkscape:connector-curvature="0" | |
| 971 | style="fill:#ebebeb;stroke-width:0.11881336" /> | |
| 972 | <path | |
| 973 | id="path4824" | |
| 974 | d="m 650.89379,111.3652 h -13.49355 v 5.40508 h 12.99555 m -0.48615,5.53605 h -12.5094 v 5.40508 h 6.64006 l -0.62843,7.02423 -6.01163,1.61914 v 5.61938 l 11.02724,-3.07161" | |
| 975 | inkscape:connector-curvature="0" | |
| 976 | style="fill:#ffffff;stroke-width:0.11881336" /> | |
| 977 | <path | |
| 978 | sodipodi:nodetypes="cc" | |
| 979 | inkscape:connector-curvature="0" | |
| 980 | id="path5804" | |
| 981 | d="m 240.99252,105.07517 65.2338,-1.01308" | |
| 982 | style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9479)" /> | |
| 983 | <path | |
| 984 | style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9505)" | |
| 985 | d="m 481.61302,105.07517 65.2337,-1.01308" | |
| 986 | id="path9497" | |
| 987 | inkscape:connector-curvature="0" | |
| 988 | sodipodi:nodetypes="cc" /> | |
| 989 | <path | |
| 990 | inkscape:connector-curvature="0" | |
| 991 | style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | |
| 992 | d="m 235.85308,230.53494 c 0.87012,0 1.65488,0.35019 2.22266,0.91797 0.56778,0.56778 0.91797,1.35253 0.91797,2.22265 v -0.23242 c 0,-1.6112 -1.297,-2.9082 -2.9082,-2.9082 z" | |
| 993 | id="path6102" /> | |
| 994 | <path | |
| 995 | inkscape:connector-curvature="0" | |
| 996 | style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" | |
| 997 | d="m 76.280823,230.53494 c -1.611195,0 -2.908203,1.297 -2.908203,2.9082 v 0.23242 c 0,-0.87012 0.35019,-1.65487 0.917968,-2.22265 0.567779,-0.56778 1.354485,-0.91797 2.22461,-0.91797 z" | |
| 998 | id="path6106" /> | |
| 999 | <path | |
| 1000 | inkscape:connector-curvature="0" | |
| 1001 | style="fill:#46c7f0;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2950)" | |
| 1002 | d="m 76.515198,222.53494 c -0.870125,0 -1.656831,0.35019 -2.22461,0.91797 -0.567778,0.56778 -0.917968,1.35253 -0.917968,2.22265 v 29.71485 h 165.62109 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91797,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z" | |
| 1003 | id="path6104" | |
| 1004 | sodipodi:nodetypes="sssccssss" /> | |
| 1005 | <path | |
| 1006 | sodipodi:nodetypes="ccssssc" | |
| 1007 | inkscape:connector-curvature="0" | |
| 1008 | style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2950)" | |
| 1009 | d="M 238.99371,255.39041 H 73.37262 v 152.63392 c 0,1.6112 1.297008,2.90821 2.908203,2.90821 H 236.08551 c 1.6112,0 2.9082,-1.29701 2.9082,-2.90821 z" | |
| 1010 | id="path6100" /> | |
| 1011 | <path | |
| 1012 | style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9509)" | |
| 1013 | d="m 240.99257,328.95043 65.23376,-1.01307" | |
| 1014 | id="path9485" | |
| 1015 | inkscape:connector-curvature="0" | |
| 1016 | sodipodi:nodetypes="cc" /> | |
| 1017 | <path | |
| 1018 | sodipodi:nodetypes="cc" | |
| 1019 | inkscape:connector-curvature="0" | |
| 1020 | id="path9757" | |
| 1021 | d="m 481.61298,300.08996 65.23376,-1.01307" | |
| 1022 | style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9761)" /> | |
| 1023 | <path | |
| 1024 | style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9767)" | |
| 1025 | d="M 552.61456,372.04139 487.3808,371.02832" | |
| 1026 | id="path9763" | |
| 1027 | inkscape:connector-curvature="0" | |
| 1028 | sodipodi:nodetypes="cc" /> | |
| 1029 | <text | |
| 1030 | id="text2269" | |
| 1031 | y="62.149761" | |
| 1032 | x="115.43707" | |
| 1033 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1034 | xml:space="preserve"><tspan | |
| 1035 | y="62.149761" | |
| 1036 | x="115.43707" | |
| 1037 | id="tspan2267" | |
| 1038 | sodipodi:role="line">Text Edit</tspan></text> | |
| 1039 | <text | |
| 1040 | transform="rotate(-90)" | |
| 1041 | id="text2273" | |
| 1042 | y="43.507812" | |
| 1043 | x="-132.24059" | |
| 1044 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1045 | xml:space="preserve"><tspan | |
| 1046 | y="43.507812" | |
| 1047 | x="-132.24059" | |
| 1048 | id="tspan2271" | |
| 1049 | sodipodi:role="line">Today</tspan></text> | |
| 1050 | <text | |
| 1051 | id="text2277" | |
| 1052 | y="61.540386" | |
| 1053 | x="358.88168" | |
| 1054 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1055 | xml:space="preserve"><tspan | |
| 1056 | y="61.540386" | |
| 1057 | x="358.88168" | |
| 1058 | id="tspan2275" | |
| 1059 | sodipodi:role="line">Process</tspan></text> | |
| 1060 | <text | |
| 1061 | id="text2281" | |
| 1062 | y="59.34898" | |
| 1063 | x="605.30872" | |
| 1064 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1065 | xml:space="preserve"><tspan | |
| 1066 | y="59.34898" | |
| 1067 | x="605.30872" | |
| 1068 | id="tspan2279" | |
| 1069 | sodipodi:role="line">Output</tspan></text> | |
| 1070 | <text | |
| 1071 | id="text2285" | |
| 1072 | y="245.17946" | |
| 1073 | x="605.30872" | |
| 1074 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1075 | xml:space="preserve"><tspan | |
| 1076 | y="245.17946" | |
| 1077 | x="605.30872" | |
| 1078 | id="tspan2283" | |
| 1079 | sodipodi:role="line">Output</tspan></text> | |
| 1080 | <text | |
| 1081 | id="text2289" | |
| 1082 | y="247.37088" | |
| 1083 | x="358.88168" | |
| 1084 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1085 | xml:space="preserve"><tspan | |
| 1086 | y="247.37088" | |
| 1087 | x="358.88168" | |
| 1088 | id="tspan2287" | |
| 1089 | sodipodi:role="line">Process</tspan></text> | |
| 1090 | <text | |
| 1091 | id="text2293" | |
| 1092 | y="247.98026" | |
| 1093 | x="115.43707" | |
| 1094 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1095 | xml:space="preserve"><tspan | |
| 1096 | y="247.98026" | |
| 1097 | x="115.43707" | |
| 1098 | id="tspan2291" | |
| 1099 | sodipodi:role="line">Text Edit</tspan></text> | |
| 1100 | <text | |
| 1101 | transform="rotate(-90)" | |
| 1102 | id="text2297" | |
| 1103 | y="43.630859" | |
| 1104 | x="-363.15442" | |
| 1105 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1106 | xml:space="preserve"><tspan | |
| 1107 | y="43.630859" | |
| 1108 | x="-363.15442" | |
| 1109 | id="tspan2295" | |
| 1110 | sodipodi:role="line">Proposed</tspan></text> | |
| 1111 | <text | |
| 1112 | id="text2301" | |
| 1113 | y="314.01108" | |
| 1114 | x="98.034729" | |
| 1115 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none" | |
| 1116 | xml:space="preserve"><tspan | |
| 1117 | id="tspan2299" | |
| 1118 | sodipodi:role="line" | |
| 1119 | x="98.034729" | |
| 1120 | y="314.01108">R Markdown</tspan></text> | |
| 1121 | <text | |
| 1122 | id="text2305" | |
| 1123 | y="285.84311" | |
| 1124 | x="107.43903" | |
| 1125 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none" | |
| 1126 | xml:space="preserve"><tspan | |
| 1127 | id="tspan2303" | |
| 1128 | sodipodi:role="line" | |
| 1129 | x="107.43903" | |
| 1130 | y="285.84311">Markdown</tspan></text> | |
| 1131 | <text | |
| 1132 | id="text2309" | |
| 1133 | y="342.91147" | |
| 1134 | x="134.3277" | |
| 1135 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none" | |
| 1136 | xml:space="preserve"><tspan | |
| 1137 | id="tspan2307" | |
| 1138 | sodipodi:role="line" | |
| 1139 | x="134.3277" | |
| 1140 | y="342.91147">XML</tspan></text> | |
| 1141 | <text | |
| 1142 | id="text2313" | |
| 1143 | y="370.34702" | |
| 1144 | x="113.56207" | |
| 1145 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none" | |
| 1146 | xml:space="preserve"><tspan | |
| 1147 | id="tspan2311" | |
| 1148 | sodipodi:role="line" | |
| 1149 | x="113.56207" | |
| 1150 | y="370.34702">DocBook</tspan></text> | |
| 1151 | <text | |
| 1152 | id="text2317" | |
| 1153 | y="398.51498" | |
| 1154 | x="114.3526" | |
| 1155 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none" | |
| 1156 | xml:space="preserve"><tspan | |
| 1157 | id="tspan2315" | |
| 1158 | sodipodi:role="line" | |
| 1159 | x="114.3526" | |
| 1160 | y="398.51498">AsciiDoc</tspan></text> | |
| 1161 | <text | |
| 1162 | transform="rotate(-90)" | |
| 1163 | id="text2329" | |
| 1164 | y="43.507812" | |
| 1165 | x="-774.87335" | |
| 1166 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1167 | xml:space="preserve"><tspan | |
| 1168 | y="43.507812" | |
| 1169 | x="-774.87335" | |
| 1170 | id="tspan2327" | |
| 1171 | sodipodi:role="line">Example Processing Combination</tspan></text> | |
| 1172 | <text | |
| 1173 | id="text2333" | |
| 1174 | y="562.05426" | |
| 1175 | x="135.31207" | |
| 1176 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#f3fbfe;fill-opacity:1;stroke:none" | |
| 1177 | xml:space="preserve"><tspan | |
| 1178 | y="562.05426" | |
| 1179 | x="135.31207" | |
| 1180 | id="tspan2331" | |
| 1181 | sodipodi:role="line">XML</tspan></text> | |
| 1182 | <text | |
| 1183 | id="text2337" | |
| 1184 | y="495.6918" | |
| 1185 | x="381.64142" | |
| 1186 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#333333;fill-opacity:0.866667;stroke:none" | |
| 1187 | xml:space="preserve"><tspan | |
| 1188 | y="495.6918" | |
| 1189 | x="381.64142" | |
| 1190 | id="tspan2335" | |
| 1191 | sodipodi:role="line">XSLT</tspan></text> | |
| 1192 | <text | |
| 1193 | id="text2341" | |
| 1194 | y="562.05426" | |
| 1195 | x="323.97742" | |
| 1196 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1197 | xml:space="preserve"><tspan | |
| 1198 | y="562.05426" | |
| 1199 | x="323.97742" | |
| 1200 | id="tspan2339" | |
| 1201 | sodipodi:role="line">XSLT Processor</tspan></text> | |
| 1202 | <text | |
| 1203 | id="text2345" | |
| 1204 | y="562.54059" | |
| 1205 | x="579.27557" | |
| 1206 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1207 | xml:space="preserve"><tspan | |
| 1208 | y="562.54059" | |
| 1209 | x="579.27557" | |
| 1210 | id="tspan2343" | |
| 1211 | sodipodi:role="line">R Markdown</tspan></text> | |
| 1212 | <text | |
| 1213 | id="text2349" | |
| 1214 | y="643.24084" | |
| 1215 | x="588.75018" | |
| 1216 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1217 | xml:space="preserve"><tspan | |
| 1218 | y="643.24084" | |
| 1219 | x="588.75018" | |
| 1220 | id="tspan2347" | |
| 1221 | sodipodi:role="line">Markdown</tspan></text> | |
| 1222 | <text | |
| 1223 | id="text2353" | |
| 1224 | y="642.63147" | |
| 1225 | x="339.61023" | |
| 1226 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1227 | xml:space="preserve"><tspan | |
| 1228 | y="642.63147" | |
| 1229 | x="339.61023" | |
| 1230 | id="tspan2351" | |
| 1231 | sodipodi:role="line">R Processor</tspan></text> | |
| 1232 | <text | |
| 1233 | id="text2357" | |
| 1234 | y="722.93903" | |
| 1235 | x="318.43912" | |
| 1236 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:21.3333px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1237 | xml:space="preserve"><tspan | |
| 1238 | y="722.93903" | |
| 1239 | x="318.43912" | |
| 1240 | id="tspan2355" | |
| 1241 | sodipodi:role="line">Variable Processor</tspan></text> | |
| 1242 | <text | |
| 1243 | id="text2361" | |
| 1244 | y="723.3316" | |
| 1245 | x="604.07831" | |
| 1246 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1247 | xml:space="preserve"><tspan | |
| 1248 | y="723.3316" | |
| 1249 | x="604.07831" | |
| 1250 | id="tspan2359" | |
| 1251 | sodipodi:role="line">HTML5</tspan></text> | |
| 1252 | <text | |
| 1253 | id="text2365" | |
| 1254 | y="630.84766" | |
| 1255 | x="81.211723" | |
| 1256 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#333333;fill-opacity:0.866667;stroke:none" | |
| 1257 | xml:space="preserve"><tspan | |
| 1258 | y="630.84766" | |
| 1259 | x="81.211723" | |
| 1260 | id="tspan2363" | |
| 1261 | sodipodi:role="line">Structured Data Source</tspan></text> | |
| 1262 | <text | |
| 1263 | id="text2369" | |
| 1264 | y="756.39404" | |
| 1265 | x="215.65826" | |
| 1266 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none" | |
| 1267 | xml:space="preserve"><tspan | |
| 1268 | y="756.39404" | |
| 1269 | x="215.65826" | |
| 1270 | id="tspan2367" | |
| 1271 | sodipodi:role="line">interpolated values</tspan></text> | |
| 1272 | <g | |
| 1273 | transform="translate(-0.25585322,11.831789)" | |
| 1274 | id="g2523"> | |
| 1275 | <text | |
| 1276 | xml:space="preserve" | |
| 1277 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1278 | x="156.49219" | |
| 1279 | y="708.2467" | |
| 1280 | id="text2373"><tspan | |
| 1281 | sodipodi:role="line" | |
| 1282 | id="tspan2371" | |
| 1283 | x="156.49219" | |
| 1284 | y="708.2467">CSON</tspan></text> | |
| 1285 | <text | |
| 1286 | xml:space="preserve" | |
| 1287 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1288 | x="156.49219" | |
| 1289 | y="688.41504" | |
| 1290 | id="text2377"><tspan | |
| 1291 | sodipodi:role="line" | |
| 1292 | id="tspan2375" | |
| 1293 | x="156.49219" | |
| 1294 | y="688.41504">JSONNET</tspan></text> | |
| 1295 | <text | |
| 1296 | xml:space="preserve" | |
| 1297 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1298 | x="156.49219" | |
| 1299 | y="668.24695" | |
| 1300 | id="text2381"><tspan | |
| 1301 | sodipodi:role="line" | |
| 1302 | id="tspan2379" | |
| 1303 | x="156.49219" | |
| 1304 | y="668.24695">JSON5</tspan></text> | |
| 1305 | <text | |
| 1306 | xml:space="preserve" | |
| 1307 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1308 | x="156.49219" | |
| 1309 | y="648.07886" | |
| 1310 | id="text2385"><tspan | |
| 1311 | sodipodi:role="line" | |
| 1312 | id="tspan2383" | |
| 1313 | x="156.49219" | |
| 1314 | y="648.07886">JSON</tspan></text> | |
| 1315 | <text | |
| 1316 | xml:space="preserve" | |
| 1317 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1318 | x="94.110725" | |
| 1319 | y="648.41534" | |
| 1320 | id="text2389"><tspan | |
| 1321 | sodipodi:role="line" | |
| 1322 | id="tspan2387" | |
| 1323 | x="94.110725" | |
| 1324 | y="648.41534">YAML</tspan></text> | |
| 1325 | <text | |
| 1326 | xml:space="preserve" | |
| 1327 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1328 | x="94.110725" | |
| 1329 | y="668.24695" | |
| 1330 | id="text2393"><tspan | |
| 1331 | sodipodi:role="line" | |
| 1332 | id="tspan2391" | |
| 1333 | x="94.110725" | |
| 1334 | y="668.24695">TOML</tspan></text> | |
| 1335 | <text | |
| 1336 | xml:space="preserve" | |
| 1337 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none" | |
| 1338 | x="94.110725" | |
| 1339 | y="688.41504" | |
| 1340 | id="text2397"><tspan | |
| 1341 | sodipodi:role="line" | |
| 1342 | id="tspan2395" | |
| 1343 | x="94.110725" | |
| 1344 | y="688.41504">XML</tspan></text> | |
| 1345 | </g> | |
| 1346 | <g | |
| 1347 | transform="translate(-1.2304677,-0.85937628)" | |
| 1348 | id="g2593"> | |
| 1349 | <g | |
| 1350 | id="g2532"> | |
| 1351 | <rect | |
| 1352 | id="rect4698" | |
| 1353 | ry="2.7292624" | |
| 1354 | y="91.740654" | |
| 1355 | x="129.16347" | |
| 1356 | height="32.205296" | |
| 1357 | width="54.039394" | |
| 1358 | style="fill:none;stroke:#05556e;stroke-width:2.72926;stroke-opacity:1" /> | |
| 1359 | <path | |
| 1360 | style="fill:#05556e;fill-opacity:1;stroke-width:0.272926" | |
| 1361 | id="path4700" | |
| 1362 | d="M 135.98663,117.12279 V 98.56381 h 5.45852 l 5.45853,6.82315 5.45852,-6.82315 h 5.45853 v 18.55898 h -5.45853 v -10.64412 l -5.45852,6.82315 -5.45853,-6.82315 v 10.64412 z m 34.11578,0 -8.18779,-9.00657 h 5.45852 v -9.55241 h 5.45853 v 9.55241 h 5.45852 z" | |
| 1363 | inkscape:connector-curvature="0" /> | |
| 1364 | </g> | |
| 1365 | <text | |
| 1366 | xml:space="preserve" | |
| 1367 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none" | |
| 1368 | x="108.73981" | |
| 1369 | y="152.80437" | |
| 1370 | id="text2407"><tspan | |
| 1371 | sodipodi:role="line" | |
| 1372 | id="tspan2405" | |
| 1373 | x="108.73981" | |
| 1374 | y="152.80437">Markdown</tspan></text> | |
| 1375 | </g> | |
| 1376 | <path | |
| 1377 | inkscape:connector-curvature="0" | |
| 1378 | d="m 417.86562,272.90923 c -2.81873,0.35302 -5.58858,1.78683 -7.90222,4.10047 -1.79226,1.78682 -3.43787,4.20365 -5.01832,7.35911 -1.28173,2.56347 -2.29191,5.21927 -2.90019,7.59265 l -0.1738,0.68975 -0.68975,0.35302 c -0.96673,0.49423 -1.81398,1.01561 -2.77528,1.69993 -3.29666,2.35709 -6.15341,5.19211 -8.53222,8.46705 -0.23354,0.32586 -0.45621,0.58656 -0.49966,0.58656 -0.038,0 -0.33673,-0.0435 -0.65716,-0.0923 -0.73863,-0.11949 -3.19891,-0.13578 -4.11676,-0.0272 -3.79633,0.46164 -7.25593,1.57502 -11.41613,3.68228 -3.00339,1.5207 -4.93685,2.87304 -6.8323,4.77391 -2.37881,2.37882 -3.80176,5.01832 -4.21452,7.82076 -0.0978,0.62457 -0.0978,2.39511 0,3.0414 0.51052,3.55193 2.55804,6.94636 5.27358,8.74404 3.15003,2.08554 7.40256,2.6558 12.27424,1.65105 3.62253,-0.75492 7.20161,-2.14527 10.77526,-4.19822 3.47046,-1.99321 5.87643,-4.18193 7.57093,-6.87575 0.27155,-0.43449 0.35845,-0.52682 0.53224,-0.59199 2.79701,-1.01018 4.74677,-2.05295 6.96265,-3.72572 2.02036,-1.5207 3.43244,-2.85675 6.0991,-5.77324 0.68432,-0.74949 0.8038,-0.91785 0.84182,-1.16225 0.0326,-0.17379 0.0543,-0.20095 0.15207,-0.17922 0.51595,0.10319 2.20502,0.11948 2.94908,0.0272 2.08553,-0.25526 4.05701,-1.10251 6.01763,-2.57976 2.61778,-1.97691 5.06177,-5.27901 6.78885,-9.17853 2.59606,-5.86556 3.57908,-10.80785 3.01425,-15.19073 -0.14121,-1.12423 -0.28241,-1.74881 -0.59742,-2.71554 -0.42905,-1.29803 -1.08621,-2.55804 -1.89001,-3.62796 -0.43449,-0.57026 -1.57502,-1.70536 -2.14528,-2.12898 -1.59131,-1.17855 -3.93753,-2.13442 -6.03936,-2.46028 -0.66259,-0.10319 -2.29735,-0.14664 -2.85132,-0.0815 z m 2.44399,7.82076 c 1.94433,0.46707 3.2152,2.04751 3.5302,4.39917 0.0815,0.58656 0.0815,2.10183 0,2.7427 -0.32043,2.62864 -1.26544,5.70263 -2.61235,8.48878 -1.01561,2.10725 -1.79226,3.34011 -2.88933,4.58383 -0.32587,0.36931 -1.38493,1.31975 -1.42838,1.2763 -0.005,-0.005 0.0706,-0.34216 0.1738,-0.74406 0.24983,-0.97759 0.34215,-1.56958 0.3856,-2.41683 0.0706,-1.58044 -0.27155,-3.09571 -0.98302,-4.30684 -1.20027,-2.05295 -3.17175,-3.41072 -5.47453,-3.78547 -0.11405,-0.0163 -0.20638,-0.0489 -0.20638,-0.076 0,-0.0217 0.19552,-0.53768 0.42905,-1.15139 1.41752,-3.67684 2.66666,-5.83298 4.30142,-7.40799 1.0482,-1.01562 1.70536,-1.40665 2.73726,-1.62933 0.51596,-0.11405 1.49355,-0.0978 2.03666,0.0272 z m -10.34078,17.93885 c 0.52139,0.54311 0.56483,0.76579 0.46164,2.25933 l -0.0326,0.51596 -0.14121,-0.21725 c -0.22811,-0.34215 -0.40733,-0.72233 -0.52682,-1.1188 -0.0652,-0.20095 -0.15207,-0.43992 -0.20095,-0.53224 -0.0706,-0.13035 -0.17922,-0.91243 -0.19008,-1.34691 0,-0.11949 0.29871,0.0923 0.63,0.43991 z m -7.36997,3.01425 c 0.3856,2.28649 1.18397,4.05159 2.44941,5.40393 l 0.45078,0.47793 -0.13577,0.14664 c -0.0706,0.0815 -0.46165,0.51052 -0.86355,0.9613 -1.55328,1.73795 -2.81873,2.98167 -4.05158,3.97012 -0.41819,0.34216 -0.78208,0.61915 -0.79837,0.61915 -0.0163,0 -0.0435,-0.0923 -0.0652,-0.20638 -0.076,-0.4019 -0.46708,-1.4664 -0.8038,-2.15614 -0.54311,-1.12424 -1.14596,-2.0095 -2.08554,-3.0577 l -0.45621,-0.50509 0.41276,-0.50509 c 1.19484,-1.47182 2.92192,-3.26951 4.43177,-4.62728 0.85811,-0.76578 1.37949,-1.21656 1.39578,-1.20027 0.005,0.005 0.0597,0.315 0.11949,0.67888 z m -16.52135,9.77052 c -0.0163,0.11405 -0.0815,0.54311 -0.14664,0.9613 -0.22267,1.47182 -0.23353,3.57365 -0.0272,4.78478 0.19008,1.10251 0.57569,2.11812 1.08078,2.81873 0.27699,0.38018 0.87441,0.97759 1.22199,1.20027 l 0.23354,0.1575 -0.15207,0.12492 c -0.60285,0.48879 -2.54174,1.58044 -4.18193,2.34622 -2.4114,1.12967 -4.36659,1.7651 -6.62049,2.16157 -0.77664,0.13578 -0.99932,0.15207 -2.09096,0.15207 -0.98846,0 -1.30889,-0.0217 -1.67278,-0.0978 -1.5207,-0.33672 -2.53088,-0.97216 -3.1989,-2.0095 -0.53225,-0.82552 -0.72234,-1.48268 -0.72777,-2.43855 0,-1.56415 0.57027,-2.68296 2.17244,-4.27969 1.78682,-1.77597 3.93753,-3.05227 7.72299,-4.5784 2.01493,-0.81467 4.20366,-1.37407 5.75151,-1.4664 0.74406,-0.0434 0.66803,-0.0652 0.63544,0.16294 z m 6.13712,3.5302 c -0.0163,0.0543 -0.0272,0.0109 -0.0272,-0.0923 0,-0.10319 0.0109,-0.14664 0.0272,-0.0978 0.0109,0.0543 0.0109,0.14121 0,0.19009 z" | |
| 1379 | id="path8164" | |
| 1380 | style="fill:#df4d65;fill-opacity:1;stroke:none;stroke-width:0.00543108" /> | |
| 1381 | <g | |
| 1382 | transform="translate(1.378418e-5,1.0193503)" | |
| 1383 | id="g1168"> | |
| 1384 | <text | |
| 1385 | id="text1158" | |
| 1386 | y="364.17905" | |
| 1387 | x="349.05551" | |
| 1388 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none" | |
| 1389 | xml:space="preserve"><tspan | |
| 1390 | id="tspan1156" | |
| 1391 | sodipodi:role="line" | |
| 1392 | x="349.05551" | |
| 1393 | y="364.17905">Processor</tspan></text> | |
| 1394 | <text | |
| 1395 | xml:space="preserve" | |
| 1396 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none" | |
| 1397 | x="370.40707" | |
| 1398 | y="392.17905" | |
| 1399 | id="text1162"><tspan | |
| 1400 | y="392.17905" | |
| 1401 | x="370.40707" | |
| 1402 | sodipodi:role="line" | |
| 1403 | id="tspan1160">Chain</tspan></text> | |
| 1404 | </g> | |
| 1405 | <g | |
| 1406 | transform="translate(0,-2.3144459)" | |
| 1407 | id="g1206"> | |
| 1408 | <text | |
| 1409 | xml:space="preserve" | |
| 1410 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none" | |
| 1411 | x="586.44855" | |
| 1412 | y="327.56967" | |
| 1413 | id="text1190"><tspan | |
| 1414 | y="327.56967" | |
| 1415 | x="586.44855" | |
| 1416 | sodipodi:role="line" | |
| 1417 | id="tspan1188">Processor-</tspan></text> | |
| 1418 | <text | |
| 1419 | id="text1194" | |
| 1420 | y="355.56967" | |
| 1421 | x="588.43488" | |
| 1422 | style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none" | |
| 1423 | xml:space="preserve"><tspan | |
| 1424 | id="tspan1192" | |
| 1425 | sodipodi:role="line" | |
| 1426 | x="588.43488" | |
| 1427 | y="355.56967">dependent</tspan></text> | |
| 1428 | </g> | |
| 1429 | </svg> | |
| 1 | 1430 |
| 1 | ||
| 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> | |
| 2 | <title>HTML5 Logo</title> | |
| 3 | <path d="M108.4 0h23v22.8h21.2V0h23v69h-23V46h-21v23h-23.2M206 23h-20.3V0h63.7v23H229v46h-23M259.5 0h24.1l14.8 24.3L313.2 0h24.1v69h-23V34.8l-16.1 24.8l-16.1-24.8v34.2h-22.6M348.7 0h23v46.2h32.6V69h-55.6"/> | |
| 4 | <path fill="#e44d26" d="M107.6 471l-33-370.4h362.8l-33 370.2L255.7 512"/> | |
| 5 | <path fill="#f16529" d="M256 480.5V131H404.3L376 447"/> | |
| 6 | <path fill="#ebebeb" d="M142 176.3h114v45.4h-64.2l4.2 46.5h60v45.3H154.4M156.4 336.3H202l3.2 36.3 50.8 13.6v47.4l-93.2-26"/> | |
| 7 | <path fill="#fff" d="M369.6 176.3H255.8v45.4h109.6M361.3 268.2H255.8v45.4h56l-5.3 59-50.7 13.6v47.2l93-25.8"/> | |
| 8 | </svg> |
| 1 | <?xml version="1.0" standalone="no"?> | |
| 2 | <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" | |
| 3 | "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> | |
| 4 | <svg version="1.0" xmlns="http://www.w3.org/2000/svg" | |
| 5 | width="1280.000000pt" height="1123.000000pt" viewBox="0 0 1280.000000 1123.000000" | |
| 6 | preserveAspectRatio="xMidYMid meet"> | |
| 7 | <metadata> | |
| 8 | Created by potrace 1.15, written by Peter Selinger 2001-2017 | |
| 9 | </metadata> | |
| 10 | <g transform="translate(0.000000,1123.000000) scale(0.100000,-0.100000)" | |
| 11 | fill="#000000" stroke="none"> | |
| 12 | <path d="M10280 11220 c-519 -65 -1029 -329 -1455 -755 -330 -329 -633 -774 | |
| 13 | -924 -1355 -236 -472 -422 -961 -534 -1398 l-32 -127 -127 -65 c-178 -91 -334 | |
| 14 | -187 -511 -313 -607 -434 -1133 -956 -1571 -1559 -43 -60 -84 -108 -92 -108 | |
| 15 | -7 0 -62 8 -121 17 -136 22 -589 25 -758 5 -699 -85 -1336 -290 -2102 -678 | |
| 16 | -553 -280 -909 -529 -1258 -879 -438 -438 -700 -924 -776 -1440 -18 -115 -18 | |
| 17 | -441 0 -560 94 -654 471 -1279 971 -1610 580 -384 1363 -489 2260 -304 667 | |
| 18 | 139 1326 395 1984 773 639 367 1082 770 1394 1266 50 80 66 97 98 109 515 186 | |
| 19 | 874 378 1282 686 372 280 632 526 1123 1063 126 138 148 169 155 214 6 32 10 | |
| 20 | 37 28 33 95 -19 406 -22 543 -5 384 47 747 203 1108 475 482 364 932 972 1250 | |
| 21 | 1690 478 1080 659 1990 555 2797 -26 207 -52 322 -110 500 -79 239 -200 471 | |
| 22 | -348 668 -80 105 -290 314 -395 392 -293 217 -725 393 -1112 453 -122 19 -423 | |
| 23 | 27 -525 15z m450 -1440 c358 -86 592 -377 650 -810 15 -108 15 -387 0 -505 | |
| 24 | -59 -484 -233 -1050 -481 -1563 -187 -388 -330 -615 -532 -844 -60 -68 -255 | |
| 25 | -243 -263 -235 -1 1 13 63 32 137 46 180 63 289 71 445 13 291 -50 570 -181 | |
| 26 | 793 -221 378 -584 628 -1008 697 -21 3 -38 9 -38 14 0 4 36 99 79 212 261 677 | |
| 27 | 491 1074 792 1364 193 187 314 259 504 300 95 21 275 18 375 -5z m-1904 -3303 | |
| 28 | c96 -100 104 -141 85 -416 l-6 -95 -26 40 c-42 63 -75 133 -97 206 -12 37 -28 | |
| 29 | 81 -37 98 -13 24 -33 168 -35 248 0 22 55 -17 116 -81z m-1357 -555 c71 -421 | |
| 30 | 218 -746 451 -995 l83 -88 -25 -27 c-13 -15 -85 -94 -159 -177 -286 -320 -519 | |
| 31 | -549 -746 -731 -77 -63 -144 -114 -147 -114 -3 0 -8 17 -12 38 -14 74 -86 270 | |
| 32 | -148 397 -100 207 -211 370 -384 563 l-84 93 76 93 c220 271 538 602 816 852 | |
| 33 | 158 141 254 224 257 221 1 -1 11 -58 22 -125z m-3042 -1799 c-3 -21 -15 -100 | |
| 34 | -27 -177 -41 -271 -43 -658 -5 -881 35 -203 106 -390 199 -519 51 -70 161 | |
| 35 | -180 225 -221 l43 -29 -28 -23 c-111 -90 -468 -291 -770 -432 -444 -208 -804 | |
| 36 | -325 -1219 -398 -143 -25 -184 -28 -385 -28 -182 0 -241 4 -308 18 -280 62 | |
| 37 | -466 179 -589 370 -98 152 -133 273 -134 449 0 288 105 494 400 788 329 327 | |
| 38 | 725 562 1422 843 371 150 774 253 1059 270 137 8 123 12 117 -30z m1130 -650 | |
| 39 | c-3 -10 -5 -2 -5 17 0 19 2 27 5 18 2 -10 2 -26 0 -35z"/> | |
| 40 | </g> | |
| 41 | </svg> | |
| 1 | 42 |
| 1 | ||
| 1 | <svg xmlns="http://www.w3.org/2000/svg" width="208" height="128" viewBox="0 0 208 128"><rect width="198" height="118" x="5" y="5" ry="10" stroke="#000" stroke-width="10" fill="none"/><path d="M30 98V30h20l20 25 20-25h20v68H90V59L70 84 50 59v39zm125 0l-30-33h20V30h20v35h20z"/></svg> |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar; | |
| 29 | ||
| 30 | import org.tbee.javafx.scene.layout.fxml.MigPane; | |
| 31 | ||
| 32 | /** | |
| 33 | * Hides dependency on {@link MigPane} from subclasses. | |
| 34 | */ | |
| 35 | public abstract class AbstractPane extends MigPane { | |
| 36 | } | |
| 37 | 1 |
| 30 | 30 | import com.scrivenvar.service.Settings; |
| 31 | 31 | |
| 32 | import java.nio.file.Path; | |
| 33 | import java.nio.file.Paths; | |
| 34 | ||
| 32 | 35 | /** |
| 33 | 36 | * Defines application-wide default values. |
| ... | ||
| 100 | 103 | public static final int DEFAULT_MAP_SIZE = 64; |
| 101 | 104 | |
| 105 | /** | |
| 106 | * Default image extension order to use when scanning. | |
| 107 | */ | |
| 102 | 108 | public static final String PERSIST_IMAGES_DEFAULT = |
| 103 | get( "file.stylesheet.scene" ); | |
| 109 | get( "file.ext.image.order" ); | |
| 104 | 110 | |
| 105 | 111 | /** |
| 106 | 112 | * Default working directory to use for R startup script. |
| 107 | 113 | */ |
| 108 | 114 | public static final String USER_DIRECTORY = System.getProperty( "user.dir" ); |
| 115 | ||
| 116 | /** | |
| 117 | * Default path to use for an untitled (pathless) file. | |
| 118 | */ | |
| 119 | public static final Path DEFAULT_DIRECTORY = Paths.get( USER_DIRECTORY ); | |
| 120 | ||
| 121 | /** | |
| 122 | * Resource directory where different language lexicons are located. | |
| 123 | */ | |
| 124 | public static final String LEXICONS_DIRECTORY = "lexicons"; | |
| 109 | 125 | |
| 110 | 126 | /** |
| 119 | 119 | final var font = Font.createFont( Font.TRUETYPE_FONT, is ); |
| 120 | 120 | final Map attributes = font.getAttributes(); |
| 121 | ||
| 121 | 122 | attributes.put( LIGATURES, LIGATURES_ON ); |
| 122 | 123 | ge.registerFont( font.deriveFont( attributes ) ); |
| 44 | 44 | import com.scrivenvar.service.Snitch; |
| 45 | 45 | import com.scrivenvar.service.events.Notifier; |
| 46 | import com.scrivenvar.util.Action; | |
| 47 | import com.scrivenvar.util.ActionBuilder; | |
| 48 | import com.scrivenvar.util.ActionUtils; | |
| 49 | import javafx.beans.binding.Bindings; | |
| 50 | import javafx.beans.binding.BooleanBinding; | |
| 51 | import javafx.beans.property.BooleanProperty; | |
| 52 | import javafx.beans.property.SimpleBooleanProperty; | |
| 53 | import javafx.beans.value.ChangeListener; | |
| 54 | import javafx.beans.value.ObservableBooleanValue; | |
| 55 | import javafx.beans.value.ObservableValue; | |
| 56 | import javafx.collections.ListChangeListener.Change; | |
| 57 | import javafx.collections.ObservableList; | |
| 58 | import javafx.event.Event; | |
| 59 | import javafx.event.EventHandler; | |
| 60 | import javafx.geometry.Pos; | |
| 61 | import javafx.scene.Node; | |
| 62 | import javafx.scene.Scene; | |
| 63 | import javafx.scene.control.*; | |
| 64 | import javafx.scene.control.Alert.AlertType; | |
| 65 | import javafx.scene.image.Image; | |
| 66 | import javafx.scene.image.ImageView; | |
| 67 | import javafx.scene.input.Clipboard; | |
| 68 | import javafx.scene.input.ClipboardContent; | |
| 69 | import javafx.scene.input.KeyEvent; | |
| 70 | import javafx.scene.layout.BorderPane; | |
| 71 | import javafx.scene.layout.VBox; | |
| 72 | import javafx.scene.text.Text; | |
| 73 | import javafx.stage.Window; | |
| 74 | import javafx.stage.WindowEvent; | |
| 75 | import javafx.util.Duration; | |
| 76 | import org.apache.commons.lang3.SystemUtils; | |
| 77 | import org.controlsfx.control.StatusBar; | |
| 78 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 79 | import org.reactfx.value.Val; | |
| 80 | import org.xhtmlrenderer.util.XRLog; | |
| 81 | ||
| 82 | import java.nio.file.Path; | |
| 83 | import java.util.HashMap; | |
| 84 | import java.util.Map; | |
| 85 | import java.util.Observable; | |
| 86 | import java.util.Observer; | |
| 87 | import java.util.function.Function; | |
| 88 | import java.util.prefs.Preferences; | |
| 89 | ||
| 90 | import static com.scrivenvar.Constants.*; | |
| 91 | import static com.scrivenvar.Messages.get; | |
| 92 | import static com.scrivenvar.util.StageState.*; | |
| 93 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*; | |
| 94 | import static javafx.application.Platform.runLater; | |
| 95 | import static javafx.event.Event.fireEvent; | |
| 96 | import static javafx.scene.input.KeyCode.ENTER; | |
| 97 | import static javafx.scene.input.KeyCode.TAB; | |
| 98 | import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST; | |
| 99 | ||
| 100 | /** | |
| 101 | * Main window containing a tab pane in the center for file editors. | |
| 102 | */ | |
| 103 | public class MainWindow implements Observer { | |
| 104 | /** | |
| 105 | * The {@code OPTIONS} variable must be declared before all other variables | |
| 106 | * to prevent subsequent initializations from failing due to missing user | |
| 107 | * preferences. | |
| 108 | */ | |
| 109 | private final static Options OPTIONS = Services.load( Options.class ); | |
| 110 | private final static Snitch SNITCH = Services.load( Snitch.class ); | |
| 111 | private final static Notifier NOTIFIER = Services.load( Notifier.class ); | |
| 112 | ||
| 113 | private final Scene mScene; | |
| 114 | private final StatusBar mStatusBar; | |
| 115 | private final Text mLineNumberText; | |
| 116 | private final TextField mFindTextField; | |
| 117 | ||
| 118 | private final Object mMutex = new Object(); | |
| 119 | ||
| 120 | /** | |
| 121 | * Prevents re-instantiation of processing classes. | |
| 122 | */ | |
| 123 | private final Map<FileEditorTab, Processor<String>> mProcessors = | |
| 124 | new HashMap<>(); | |
| 125 | ||
| 126 | private final Map<String, String> mResolvedMap = | |
| 127 | new HashMap<>( DEFAULT_MAP_SIZE ); | |
| 128 | ||
| 129 | /** | |
| 130 | * Called when the definition data is changed. | |
| 131 | */ | |
| 132 | private final EventHandler<TreeItem.TreeModificationEvent<Event>> | |
| 133 | mTreeHandler = event -> { | |
| 134 | exportDefinitions( getDefinitionPath() ); | |
| 135 | interpolateResolvedMap(); | |
| 136 | renderActiveTab(); | |
| 137 | }; | |
| 138 | ||
| 139 | /** | |
| 140 | * Called to switch to the definition pane when the user presses the TAB key. | |
| 141 | */ | |
| 142 | private final EventHandler<? super KeyEvent> mTabKeyHandler = | |
| 143 | (EventHandler<KeyEvent>) event -> { | |
| 144 | if( event.getCode() == TAB ) { | |
| 145 | getDefinitionPane().requestFocus(); | |
| 146 | event.consume(); | |
| 147 | } | |
| 148 | }; | |
| 149 | ||
| 150 | /** | |
| 151 | * Called to inject the selected item when the user presses ENTER in the | |
| 152 | * definition pane. | |
| 153 | */ | |
| 154 | private final EventHandler<? super KeyEvent> mDefinitionKeyHandler = | |
| 155 | event -> { | |
| 156 | if( event.getCode() == ENTER ) { | |
| 157 | getVariableNameInjector().injectSelectedItem(); | |
| 158 | } | |
| 159 | }; | |
| 160 | ||
| 161 | private final ChangeListener<Integer> mCaretPositionListener = | |
| 162 | ( observable, oldPosition, newPosition ) -> { | |
| 163 | final FileEditorTab tab = getActiveFileEditorTab(); | |
| 164 | final EditorPane pane = tab.getEditorPane(); | |
| 165 | final StyleClassedTextArea editor = pane.getEditor(); | |
| 166 | ||
| 167 | getLineNumberText().setText( | |
| 168 | get( STATUS_BAR_LINE, | |
| 169 | editor.getCurrentParagraph() + 1, | |
| 170 | editor.getParagraphs().size(), | |
| 171 | editor.getCaretPosition() | |
| 172 | ) | |
| 173 | ); | |
| 174 | }; | |
| 175 | ||
| 176 | private final ChangeListener<Integer> mCaretParagraphListener = | |
| 177 | ( observable, oldIndex, newIndex ) -> | |
| 178 | scrollToParagraph( newIndex, true ); | |
| 179 | ||
| 180 | private DefinitionSource mDefinitionSource = createDefaultDefinitionSource(); | |
| 181 | private final DefinitionPane mDefinitionPane = new DefinitionPane(); | |
| 182 | private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane(); | |
| 183 | private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane( | |
| 184 | mCaretPositionListener, | |
| 185 | mCaretParagraphListener ); | |
| 186 | ||
| 187 | /** | |
| 188 | * Listens on the definition pane for double-click events. | |
| 189 | */ | |
| 190 | private final VariableNameInjector mVariableNameInjector | |
| 191 | = new VariableNameInjector( mDefinitionPane ); | |
| 192 | ||
| 193 | public MainWindow() { | |
| 194 | mStatusBar = createStatusBar(); | |
| 195 | mLineNumberText = createLineNumberText(); | |
| 196 | mFindTextField = createFindTextField(); | |
| 197 | mScene = createScene(); | |
| 198 | ||
| 199 | System.getProperties() | |
| 200 | .setProperty( "xr.util-logging.loggingEnabled", "true" ); | |
| 201 | XRLog.setLoggingEnabled( true ); | |
| 202 | ||
| 203 | initLayout(); | |
| 204 | initFindInput(); | |
| 205 | initSnitch(); | |
| 206 | initDefinitionListener(); | |
| 207 | initTabAddedListener(); | |
| 208 | initTabChangedListener(); | |
| 209 | initPreferences(); | |
| 210 | initVariableNameInjector(); | |
| 211 | ||
| 212 | NOTIFIER.addObserver( this ); | |
| 213 | } | |
| 214 | ||
| 215 | private void initLayout() { | |
| 216 | final Scene appScene = getScene(); | |
| 217 | ||
| 218 | appScene.getStylesheets().add( STYLESHEET_SCENE ); | |
| 219 | ||
| 220 | // TODO: Apply an XML syntax highlighting for XML files. | |
| 221 | // appScene.getStylesheets().add( STYLESHEET_XML ); | |
| 222 | appScene.windowProperty().addListener( | |
| 223 | ( observable, oldWindow, newWindow ) -> | |
| 224 | newWindow.setOnCloseRequest( | |
| 225 | e -> { | |
| 226 | if( !getFileEditorPane().closeAllEditors() ) { | |
| 227 | e.consume(); | |
| 228 | } | |
| 229 | } | |
| 230 | ) | |
| 231 | ); | |
| 232 | } | |
| 233 | ||
| 234 | /** | |
| 235 | * Initialize the find input text field to listen on F3, ENTER, and | |
| 236 | * ESCAPE key presses. | |
| 237 | */ | |
| 238 | private void initFindInput() { | |
| 239 | final TextField input = getFindTextField(); | |
| 240 | ||
| 241 | input.setOnKeyPressed( ( KeyEvent event ) -> { | |
| 242 | switch( event.getCode() ) { | |
| 243 | case F3: | |
| 244 | case ENTER: | |
| 245 | editFindNext(); | |
| 246 | break; | |
| 247 | case F: | |
| 248 | if( !event.isControlDown() ) { | |
| 249 | break; | |
| 250 | } | |
| 251 | case ESCAPE: | |
| 252 | getStatusBar().setGraphic( null ); | |
| 253 | getActiveFileEditorTab().getEditorPane().requestFocus(); | |
| 254 | break; | |
| 255 | } | |
| 256 | } ); | |
| 257 | ||
| 258 | // Remove when the input field loses focus. | |
| 259 | input.focusedProperty().addListener( | |
| 260 | ( focused, oldFocus, newFocus ) -> { | |
| 261 | if( !newFocus ) { | |
| 262 | getStatusBar().setGraphic( null ); | |
| 263 | } | |
| 264 | } | |
| 265 | ); | |
| 266 | } | |
| 267 | ||
| 268 | /** | |
| 269 | * Watch for changes to external files. In particular, this awaits | |
| 270 | * modifications to any XSL files associated with XML files being edited. | |
| 271 | * When | |
| 272 | * an XSL file is modified (external to the application), the snitch's ears | |
| 273 | * perk up and the file is reloaded. This keeps the XSL transformation up to | |
| 274 | * date with what's on the file system. | |
| 275 | */ | |
| 276 | private void initSnitch() { | |
| 277 | SNITCH.addObserver( this ); | |
| 278 | } | |
| 279 | ||
| 280 | /** | |
| 281 | * Listen for {@link FileEditorTabPane} to receive open definition file | |
| 282 | * event. | |
| 283 | */ | |
| 284 | private void initDefinitionListener() { | |
| 285 | getFileEditorPane().onOpenDefinitionFileProperty().addListener( | |
| 286 | ( final ObservableValue<? extends Path> file, | |
| 287 | final Path oldPath, final Path newPath ) -> { | |
| 288 | // Indirectly refresh the resolved map. | |
| 289 | resetProcessors(); | |
| 290 | ||
| 291 | openDefinitions( newPath ); | |
| 292 | ||
| 293 | // Will create new processors and therefore a new resolved map. | |
| 294 | renderActiveTab(); | |
| 295 | } | |
| 296 | ); | |
| 297 | } | |
| 298 | ||
| 299 | /** | |
| 300 | * When tabs are added, hook the various change listeners onto the new | |
| 301 | * tab sothat the preview pane refreshes as necessary. | |
| 302 | */ | |
| 303 | private void initTabAddedListener() { | |
| 304 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 305 | ||
| 306 | // Make sure the text processor kicks off when new files are opened. | |
| 307 | final ObservableList<Tab> tabs = editorPane.getTabs(); | |
| 308 | ||
| 309 | // Update the preview pane on tab changes. | |
| 310 | tabs.addListener( | |
| 311 | ( final Change<? extends Tab> change ) -> { | |
| 312 | while( change.next() ) { | |
| 313 | if( change.wasAdded() ) { | |
| 314 | // Multiple tabs can be added simultaneously. | |
| 315 | for( final Tab newTab : change.getAddedSubList() ) { | |
| 316 | final FileEditorTab tab = (FileEditorTab) newTab; | |
| 317 | ||
| 318 | initTextChangeListener( tab ); | |
| 319 | initTabKeyEventListener( tab ); | |
| 320 | initScrollEventListener( tab ); | |
| 321 | // initSyntaxListener( tab ); | |
| 322 | } | |
| 323 | } | |
| 324 | } | |
| 325 | } | |
| 326 | ); | |
| 327 | } | |
| 328 | ||
| 329 | private void initScrollEventListener( final FileEditorTab tab ) { | |
| 330 | final var scrollPane = tab.getScrollPane(); | |
| 331 | final var scrollBar = getPreviewPane().getVerticalScrollBar(); | |
| 332 | ||
| 333 | // Before the drag handler can be attached, the scroll bar for the | |
| 334 | // text editor pane must be visible. | |
| 335 | final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) -> | |
| 336 | runLater( () -> { | |
| 337 | if( newShow ) { | |
| 338 | final var handler = new ScrollEventHandler( scrollPane, scrollBar ); | |
| 339 | handler.enabledProperty().bind( tab.selectedProperty() ); | |
| 340 | } | |
| 341 | } ); | |
| 342 | ||
| 343 | Val.flatMap( scrollPane.sceneProperty(), Scene::windowProperty ) | |
| 344 | .flatMap( Window::showingProperty ) | |
| 345 | .addListener( listener ); | |
| 346 | } | |
| 347 | ||
| 348 | /** | |
| 349 | * Listen for new tab selection events. | |
| 350 | */ | |
| 351 | private void initTabChangedListener() { | |
| 352 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 353 | ||
| 354 | // Update the preview pane changing tabs. | |
| 355 | editorPane.addTabSelectionListener( | |
| 356 | ( tabPane, oldTab, newTab ) -> { | |
| 357 | if( newTab == null ) { | |
| 358 | getPreviewPane().clear(); | |
| 359 | } | |
| 360 | ||
| 361 | // If there was no old tab, then this is a first time load, which | |
| 362 | // can be ignored. | |
| 363 | if( oldTab != null ) { | |
| 364 | if( newTab != null ) { | |
| 365 | final FileEditorTab tab = (FileEditorTab) newTab; | |
| 366 | updateVariableNameInjector( tab ); | |
| 367 | process( tab ); | |
| 368 | } | |
| 369 | } | |
| 370 | } | |
| 371 | ); | |
| 372 | } | |
| 373 | ||
| 374 | /** | |
| 375 | * Reloads the preferences from the previous session. | |
| 376 | */ | |
| 377 | private void initPreferences() { | |
| 378 | initDefinitionPane(); | |
| 379 | getFileEditorPane().initPreferences(); | |
| 380 | } | |
| 381 | ||
| 382 | private void initVariableNameInjector() { | |
| 383 | updateVariableNameInjector( getActiveFileEditorTab() ); | |
| 384 | } | |
| 385 | ||
| 386 | /** | |
| 387 | * Ensure that the keyboard events are received when a new tab is added | |
| 388 | * to the user interface. | |
| 389 | * | |
| 390 | * @param tab The tab editor that can trigger keyboard events. | |
| 391 | */ | |
| 392 | private void initTabKeyEventListener( final FileEditorTab tab ) { | |
| 393 | tab.addEventFilter( KeyEvent.KEY_PRESSED, mTabKeyHandler ); | |
| 394 | } | |
| 395 | ||
| 396 | private void initTextChangeListener( final FileEditorTab tab ) { | |
| 397 | tab.addTextChangeListener( | |
| 398 | ( editor, oldValue, newValue ) -> { | |
| 399 | process( tab ); | |
| 400 | scrollToParagraph( getCurrentParagraphIndex() ); | |
| 401 | } | |
| 402 | ); | |
| 403 | } | |
| 404 | ||
| 405 | private int getCurrentParagraphIndex() { | |
| 406 | return getActiveEditorPane().getCurrentParagraphIndex(); | |
| 407 | } | |
| 408 | ||
| 409 | private void scrollToParagraph( final int id ) { | |
| 410 | scrollToParagraph( id, false ); | |
| 411 | } | |
| 412 | ||
| 413 | /** | |
| 414 | * @param id The paragraph to scroll to, will be approximated if it doesn't | |
| 415 | * exist. | |
| 416 | * @param force {@code true} means to force scrolling immediately, which | |
| 417 | * should only be attempted when it is known that the document | |
| 418 | * has been fully rendered. Otherwise the internal map of ID | |
| 419 | * attributes will be incomplete and scrolling will flounder. | |
| 420 | */ | |
| 421 | private void scrollToParagraph( final int id, final boolean force ) { | |
| 422 | synchronized( mMutex ) { | |
| 423 | final var previewPane = getPreviewPane(); | |
| 424 | final var scrollPane = previewPane.getScrollPane(); | |
| 425 | final int approxId = getActiveEditorPane().approximateParagraphId( id ); | |
| 426 | ||
| 427 | if( force ) { | |
| 428 | previewPane.scrollTo( approxId ); | |
| 429 | } | |
| 430 | else { | |
| 431 | previewPane.tryScrollTo( approxId ); | |
| 432 | } | |
| 433 | ||
| 434 | scrollPane.repaint(); | |
| 435 | } | |
| 436 | } | |
| 437 | ||
| 438 | private void updateVariableNameInjector( final FileEditorTab tab ) { | |
| 439 | getVariableNameInjector().addListener( tab ); | |
| 440 | } | |
| 441 | ||
| 442 | /** | |
| 443 | * Called whenever the preview pane becomes out of sync with the file editor | |
| 444 | * tab. This can be called when the text changes, the caret paragraph | |
| 445 | * changes, or the file tab changes. | |
| 446 | * | |
| 447 | * @param tab The file editor tab that has been changed in some fashion. | |
| 448 | */ | |
| 449 | private void process( final FileEditorTab tab ) { | |
| 450 | if( tab == null ) { | |
| 451 | return; | |
| 452 | } | |
| 453 | ||
| 454 | getPreviewPane().setPath( tab.getPath() ); | |
| 455 | ||
| 456 | final Processor<String> processor = getProcessors().computeIfAbsent( | |
| 457 | tab, p -> createProcessors( tab ) | |
| 458 | ); | |
| 459 | ||
| 460 | try { | |
| 461 | processChain( processor, tab.getEditorText() ); | |
| 462 | } catch( final Exception ex ) { | |
| 463 | error( ex ); | |
| 464 | } | |
| 465 | } | |
| 466 | ||
| 467 | /** | |
| 468 | * Executes the processing chain, operating on the given string. | |
| 469 | * | |
| 470 | * @param handler The first processor in the chain to call. | |
| 471 | * @param text The initial value of the text to process. | |
| 472 | * @return The final value of the text that was processed by the chain. | |
| 473 | */ | |
| 474 | private String processChain( Processor<String> handler, String text ) { | |
| 475 | while( handler != null && text != null ) { | |
| 476 | text = handler.process( text ); | |
| 477 | handler = handler.next(); | |
| 478 | } | |
| 479 | ||
| 480 | return text; | |
| 481 | } | |
| 482 | ||
| 483 | private void renderActiveTab() { | |
| 484 | process( getActiveFileEditorTab() ); | |
| 485 | } | |
| 486 | ||
| 487 | /** | |
| 488 | * Called when a definition source is opened. | |
| 489 | * | |
| 490 | * @param path Path to the definition source that was opened. | |
| 491 | */ | |
| 492 | private void openDefinitions( final Path path ) { | |
| 493 | try { | |
| 494 | final DefinitionSource ds = createDefinitionSource( path ); | |
| 495 | setDefinitionSource( ds ); | |
| 496 | getUserPreferences().definitionPathProperty().setValue( path.toFile() ); | |
| 497 | getUserPreferences().save(); | |
| 498 | ||
| 499 | final Tooltip tooltipPath = new Tooltip( path.toString() ); | |
| 500 | tooltipPath.setShowDelay( Duration.millis( 200 ) ); | |
| 501 | ||
| 502 | final DefinitionPane pane = getDefinitionPane(); | |
| 503 | pane.update( ds ); | |
| 504 | pane.addTreeChangeHandler( mTreeHandler ); | |
| 505 | pane.addKeyEventHandler( mDefinitionKeyHandler ); | |
| 506 | pane.filenameProperty().setValue( path.getFileName().toString() ); | |
| 507 | pane.setTooltip( tooltipPath ); | |
| 508 | ||
| 509 | interpolateResolvedMap(); | |
| 510 | } catch( final Exception e ) { | |
| 511 | error( e ); | |
| 512 | } | |
| 513 | } | |
| 514 | ||
| 515 | private void exportDefinitions( final Path path ) { | |
| 516 | try { | |
| 517 | final DefinitionPane pane = getDefinitionPane(); | |
| 518 | final TreeItem<String> root = pane.getTreeView().getRoot(); | |
| 519 | final TreeItem<String> problemChild = pane.isTreeWellFormed(); | |
| 520 | ||
| 521 | if( problemChild == null ) { | |
| 522 | getDefinitionSource().getTreeAdapter().export( root, path ); | |
| 523 | getNotifier().clear(); | |
| 524 | } | |
| 525 | else { | |
| 526 | final String msg = get( | |
| 527 | "yaml.error.tree.form", problemChild.getValue() ); | |
| 528 | getNotifier().notify( msg ); | |
| 529 | } | |
| 530 | } catch( final Exception e ) { | |
| 531 | error( e ); | |
| 532 | } | |
| 533 | } | |
| 534 | ||
| 535 | private void interpolateResolvedMap() { | |
| 536 | final Map<String, String> treeMap = getDefinitionPane().toMap(); | |
| 537 | final Map<String, String> map = new HashMap<>( treeMap ); | |
| 538 | MapInterpolator.interpolate( map ); | |
| 539 | ||
| 540 | getResolvedMap().clear(); | |
| 541 | getResolvedMap().putAll( map ); | |
| 542 | } | |
| 543 | ||
| 544 | private void initDefinitionPane() { | |
| 545 | openDefinitions( getDefinitionPath() ); | |
| 546 | } | |
| 547 | ||
| 548 | /** | |
| 549 | * Called when an exception occurs that warrants the user's attention. | |
| 550 | * | |
| 551 | * @param e The exception with a message that the user should know about. | |
| 552 | */ | |
| 553 | private void error( final Exception e ) { | |
| 554 | getNotifier().notify( e ); | |
| 555 | } | |
| 556 | ||
| 557 | //---- File actions ------------------------------------------------------- | |
| 558 | ||
| 559 | /** | |
| 560 | * Called when an {@link Observable} instance has changed. This is called | |
| 561 | * by both the {@link Snitch} service and the notify service. The @link | |
| 562 | * Snitch} service can be called for different file types, including | |
| 563 | * {@link DefinitionSource} instances. | |
| 564 | * | |
| 565 | * @param observable The observed instance. | |
| 566 | * @param value The noteworthy item. | |
| 567 | */ | |
| 568 | @Override | |
| 569 | public void update( final Observable observable, final Object value ) { | |
| 570 | if( value != null ) { | |
| 571 | if( observable instanceof Snitch && value instanceof Path ) { | |
| 572 | updateSelectedTab(); | |
| 573 | } | |
| 574 | else if( observable instanceof Notifier && value instanceof String ) { | |
| 575 | updateStatusBar( (String) value ); | |
| 576 | } | |
| 577 | } | |
| 578 | } | |
| 579 | ||
| 580 | /** | |
| 581 | * Updates the status bar to show the given message. | |
| 582 | * | |
| 583 | * @param s The message to show in the status bar. | |
| 584 | */ | |
| 585 | private void updateStatusBar( final String s ) { | |
| 586 | runLater( | |
| 587 | () -> { | |
| 588 | final int index = s.indexOf( '\n' ); | |
| 589 | final String message = s.substring( | |
| 590 | 0, index > 0 ? index : s.length() ); | |
| 591 | ||
| 592 | getStatusBar().setText( message ); | |
| 593 | } | |
| 594 | ); | |
| 595 | } | |
| 596 | ||
| 597 | /** | |
| 598 | * Called when a file has been modified. | |
| 599 | */ | |
| 600 | private void updateSelectedTab() { | |
| 601 | runLater( | |
| 602 | () -> { | |
| 603 | // Brute-force XSLT file reload by re-instantiating all processors. | |
| 604 | resetProcessors(); | |
| 605 | renderActiveTab(); | |
| 606 | } | |
| 607 | ); | |
| 608 | } | |
| 609 | ||
| 610 | /** | |
| 611 | * After resetting the processors, they will refresh anew to be up-to-date | |
| 612 | * with the files (text and definition) currently loaded into the editor. | |
| 613 | */ | |
| 614 | private void resetProcessors() { | |
| 615 | getProcessors().clear(); | |
| 616 | } | |
| 617 | ||
| 618 | //---- File actions ------------------------------------------------------- | |
| 619 | ||
| 620 | private void fileNew() { | |
| 621 | getFileEditorPane().newEditor(); | |
| 622 | } | |
| 623 | ||
| 624 | private void fileOpen() { | |
| 625 | getFileEditorPane().openFileDialog(); | |
| 626 | } | |
| 627 | ||
| 628 | private void fileClose() { | |
| 629 | getFileEditorPane().closeEditor( getActiveFileEditorTab(), true ); | |
| 630 | } | |
| 631 | ||
| 632 | /** | |
| 633 | * TODO: Upon closing, first remove the tab change listeners. (There's no | |
| 634 | * need to re-render each tab when all are being closed.) | |
| 635 | */ | |
| 636 | private void fileCloseAll() { | |
| 637 | getFileEditorPane().closeAllEditors(); | |
| 638 | } | |
| 639 | ||
| 640 | private void fileSave() { | |
| 641 | getFileEditorPane().saveEditor( getActiveFileEditorTab() ); | |
| 642 | } | |
| 643 | ||
| 644 | private void fileSaveAs() { | |
| 645 | final FileEditorTab editor = getActiveFileEditorTab(); | |
| 646 | getFileEditorPane().saveEditorAs( editor ); | |
| 647 | getProcessors().remove( editor ); | |
| 648 | ||
| 649 | try { | |
| 650 | process( editor ); | |
| 651 | } catch( final Exception ex ) { | |
| 652 | getNotifier().notify( ex ); | |
| 653 | } | |
| 654 | } | |
| 655 | ||
| 656 | private void fileSaveAll() { | |
| 657 | getFileEditorPane().saveAllEditors(); | |
| 658 | } | |
| 659 | ||
| 660 | private void fileExit() { | |
| 661 | final Window window = getWindow(); | |
| 662 | fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) ); | |
| 663 | } | |
| 664 | ||
| 665 | //---- Edit actions ------------------------------------------------------- | |
| 666 | ||
| 667 | /** | |
| 668 | * Transform the Markdown into HTML then copy that HTML into the copy | |
| 669 | * buffer. | |
| 670 | */ | |
| 671 | private void copyHtml() { | |
| 672 | final var markdown = getActiveEditorPane().getText(); | |
| 673 | final var processors = createProcessorFactory().createProcessors( | |
| 674 | getActiveFileEditorTab() | |
| 675 | ); | |
| 676 | ||
| 677 | final var chain = processors.remove( HtmlPreviewProcessor.class ); | |
| 678 | ||
| 679 | final String html = processChain( chain, markdown ); | |
| 680 | ||
| 681 | final Clipboard clipboard = Clipboard.getSystemClipboard(); | |
| 682 | final ClipboardContent content = new ClipboardContent(); | |
| 683 | content.putString( html ); | |
| 684 | clipboard.setContent( content ); | |
| 685 | } | |
| 686 | ||
| 687 | /** | |
| 688 | * Used to find text in the active file editor window. | |
| 689 | */ | |
| 690 | private void editFind() { | |
| 691 | final TextField input = getFindTextField(); | |
| 692 | getStatusBar().setGraphic( input ); | |
| 693 | input.requestFocus(); | |
| 694 | } | |
| 695 | ||
| 696 | public void editFindNext() { | |
| 697 | getActiveFileEditorTab().searchNext( getFindTextField().getText() ); | |
| 698 | } | |
| 699 | ||
| 700 | public void editPreferences() { | |
| 701 | getUserPreferences().show(); | |
| 702 | } | |
| 703 | ||
| 704 | //---- Insert actions ----------------------------------------------------- | |
| 705 | ||
| 706 | /** | |
| 707 | * Delegates to the active editor to handle wrapping the current text | |
| 708 | * selection with leading and trailing strings. | |
| 709 | * | |
| 710 | * @param leading The string to put before the selection. | |
| 711 | * @param trailing The string to put after the selection. | |
| 712 | */ | |
| 713 | private void insertMarkdown( | |
| 714 | final String leading, final String trailing ) { | |
| 715 | getActiveEditorPane().surroundSelection( leading, trailing ); | |
| 716 | } | |
| 717 | ||
| 718 | private void insertMarkdown( | |
| 719 | final String leading, final String trailing, final String hint ) { | |
| 720 | getActiveEditorPane().surroundSelection( leading, trailing, hint ); | |
| 721 | } | |
| 722 | ||
| 723 | //---- Help actions ------------------------------------------------------- | |
| 724 | ||
| 725 | private void helpAbout() { | |
| 726 | final Alert alert = new Alert( AlertType.INFORMATION ); | |
| 727 | alert.setTitle( get( "Dialog.about.title" ) ); | |
| 728 | alert.setHeaderText( get( "Dialog.about.header" ) ); | |
| 729 | alert.setContentText( get( "Dialog.about.content" ) ); | |
| 730 | alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) ); | |
| 731 | alert.initOwner( getWindow() ); | |
| 732 | ||
| 733 | alert.showAndWait(); | |
| 734 | } | |
| 735 | ||
| 736 | //---- Member creators ---------------------------------------------------- | |
| 737 | ||
| 738 | /** | |
| 739 | * Factory to create processors that are suited to different file types. | |
| 740 | * | |
| 741 | * @param tab The tab that is subjected to processing. | |
| 742 | * @return A processor suited to the file type specified by the tab's path. | |
| 743 | */ | |
| 744 | private Processor<String> createProcessors( final FileEditorTab tab ) { | |
| 745 | return createProcessorFactory().createProcessors( tab ); | |
| 746 | } | |
| 747 | ||
| 748 | private ProcessorFactory createProcessorFactory() { | |
| 749 | return new ProcessorFactory( getPreviewPane(), getResolvedMap() ); | |
| 750 | } | |
| 751 | ||
| 752 | private HTMLPreviewPane createHTMLPreviewPane() { | |
| 753 | return new HTMLPreviewPane(); | |
| 754 | } | |
| 755 | ||
| 756 | private DefinitionSource createDefaultDefinitionSource() { | |
| 757 | return new YamlDefinitionSource( getDefinitionPath() ); | |
| 758 | } | |
| 759 | ||
| 760 | private DefinitionSource createDefinitionSource( final Path path ) { | |
| 761 | try { | |
| 762 | return createDefinitionFactory().createDefinitionSource( path ); | |
| 763 | } catch( final Exception ex ) { | |
| 764 | error( ex ); | |
| 765 | return createDefaultDefinitionSource(); | |
| 766 | } | |
| 767 | } | |
| 768 | ||
| 769 | private TextField createFindTextField() { | |
| 770 | return new TextField(); | |
| 771 | } | |
| 772 | ||
| 773 | private DefinitionFactory createDefinitionFactory() { | |
| 774 | return new DefinitionFactory(); | |
| 775 | } | |
| 776 | ||
| 777 | private StatusBar createStatusBar() { | |
| 778 | return new StatusBar(); | |
| 779 | } | |
| 780 | ||
| 781 | private Scene createScene() { | |
| 782 | final SplitPane splitPane = new SplitPane( | |
| 783 | getDefinitionPane().getNode(), | |
| 784 | getFileEditorPane().getNode(), | |
| 785 | getPreviewPane().getNode() ); | |
| 786 | ||
| 787 | splitPane.setDividerPositions( | |
| 788 | getFloat( K_PANE_SPLIT_DEFINITION, .10f ), | |
| 789 | getFloat( K_PANE_SPLIT_EDITOR, .45f ), | |
| 790 | getFloat( K_PANE_SPLIT_PREVIEW, .45f ) ); | |
| 791 | ||
| 792 | getDefinitionPane().prefHeightProperty() | |
| 793 | .bind( splitPane.heightProperty() ); | |
| 794 | ||
| 795 | final BorderPane borderPane = new BorderPane(); | |
| 796 | borderPane.setPrefSize( 1024, 800 ); | |
| 797 | borderPane.setTop( createMenuBar() ); | |
| 798 | borderPane.setBottom( getStatusBar() ); | |
| 799 | borderPane.setCenter( splitPane ); | |
| 800 | ||
| 801 | final VBox statusBar = new VBox(); | |
| 802 | statusBar.setAlignment( Pos.BASELINE_CENTER ); | |
| 803 | statusBar.getChildren().add( getLineNumberText() ); | |
| 804 | getStatusBar().getRightItems().add( statusBar ); | |
| 805 | ||
| 806 | // Force preview pane refresh on Windows. | |
| 807 | splitPane.getDividers().get( 1 ).positionProperty().addListener( | |
| 808 | ( l, oValue, nValue ) -> runLater( | |
| 809 | () -> { | |
| 810 | if( SystemUtils.IS_OS_WINDOWS ) { | |
| 811 | getPreviewPane().getScrollPane().repaint(); | |
| 812 | } | |
| 813 | } | |
| 814 | ) | |
| 815 | ); | |
| 816 | ||
| 817 | return new Scene( borderPane ); | |
| 818 | } | |
| 819 | ||
| 820 | private Text createLineNumberText() { | |
| 821 | return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) ); | |
| 822 | } | |
| 823 | ||
| 824 | private Node createMenuBar() { | |
| 825 | final BooleanBinding activeFileEditorIsNull = | |
| 826 | getFileEditorPane().activeFileEditorProperty().isNull(); | |
| 827 | ||
| 828 | // File actions | |
| 829 | final Action fileNewAction = new ActionBuilder() | |
| 830 | .setText( "Main.menu.file.new" ) | |
| 831 | .setAccelerator( "Shortcut+N" ) | |
| 832 | .setIcon( FILE_ALT ) | |
| 833 | .setAction( e -> fileNew() ) | |
| 834 | .build(); | |
| 835 | final Action fileOpenAction = new ActionBuilder() | |
| 836 | .setText( "Main.menu.file.open" ) | |
| 837 | .setAccelerator( "Shortcut+O" ) | |
| 838 | .setIcon( FOLDER_OPEN_ALT ) | |
| 839 | .setAction( e -> fileOpen() ) | |
| 840 | .build(); | |
| 841 | final Action fileCloseAction = new ActionBuilder() | |
| 842 | .setText( "Main.menu.file.close" ) | |
| 843 | .setAccelerator( "Shortcut+W" ) | |
| 844 | .setAction( e -> fileClose() ) | |
| 845 | .setDisable( activeFileEditorIsNull ) | |
| 846 | .build(); | |
| 847 | final Action fileCloseAllAction = new ActionBuilder() | |
| 848 | .setText( "Main.menu.file.close_all" ) | |
| 849 | .setAction( e -> fileCloseAll() ) | |
| 850 | .setDisable( activeFileEditorIsNull ) | |
| 851 | .build(); | |
| 852 | final Action fileSaveAction = new ActionBuilder() | |
| 853 | .setText( "Main.menu.file.save" ) | |
| 854 | .setAccelerator( "Shortcut+S" ) | |
| 855 | .setIcon( FLOPPY_ALT ) | |
| 856 | .setAction( e -> fileSave() ) | |
| 857 | .setDisable( createActiveBooleanProperty( | |
| 858 | FileEditorTab::modifiedProperty ).not() ) | |
| 859 | .build(); | |
| 860 | final Action fileSaveAsAction = new ActionBuilder() | |
| 861 | .setText( "Main.menu.file.save_as" ) | |
| 862 | .setAction( e -> fileSaveAs() ) | |
| 863 | .setDisable( activeFileEditorIsNull ) | |
| 864 | .build(); | |
| 865 | final Action fileSaveAllAction = new ActionBuilder() | |
| 866 | .setText( "Main.menu.file.save_all" ) | |
| 867 | .setAccelerator( "Shortcut+Shift+S" ) | |
| 868 | .setAction( e -> fileSaveAll() ) | |
| 869 | .setDisable( Bindings.not( | |
| 870 | getFileEditorPane().anyFileEditorModifiedProperty() ) ) | |
| 871 | .build(); | |
| 872 | final Action fileExitAction = new ActionBuilder() | |
| 873 | .setText( "Main.menu.file.exit" ) | |
| 874 | .setAction( e -> fileExit() ) | |
| 875 | .build(); | |
| 876 | ||
| 877 | // Edit actions | |
| 878 | final Action editCopyHtmlAction = new ActionBuilder() | |
| 879 | .setText( Messages.get( "Main.menu.edit.copy.html" ) ) | |
| 880 | .setIcon( HTML5 ) | |
| 881 | .setAction( e -> copyHtml() ) | |
| 882 | .setDisable( activeFileEditorIsNull ) | |
| 883 | .build(); | |
| 884 | ||
| 885 | final Action editUndoAction = new ActionBuilder() | |
| 886 | .setText( "Main.menu.edit.undo" ) | |
| 887 | .setAccelerator( "Shortcut+Z" ) | |
| 888 | .setIcon( UNDO ) | |
| 889 | .setAction( e -> getActiveEditorPane().undo() ) | |
| 890 | .setDisable( createActiveBooleanProperty( | |
| 891 | FileEditorTab::canUndoProperty ).not() ) | |
| 892 | .build(); | |
| 893 | final Action editRedoAction = new ActionBuilder() | |
| 894 | .setText( "Main.menu.edit.redo" ) | |
| 895 | .setAccelerator( "Shortcut+Y" ) | |
| 896 | .setIcon( REPEAT ) | |
| 897 | .setAction( e -> getActiveEditorPane().redo() ) | |
| 898 | .setDisable( createActiveBooleanProperty( | |
| 899 | FileEditorTab::canRedoProperty ).not() ) | |
| 900 | .build(); | |
| 901 | ||
| 902 | final Action editCutAction = new ActionBuilder() | |
| 903 | .setText( Messages.get( "Main.menu.edit.cut" ) ) | |
| 904 | .setAccelerator( "Shortcut+X" ) | |
| 905 | .setIcon( CUT ) | |
| 906 | .setAction( e -> getActiveEditorPane().cut() ) | |
| 907 | .setDisable( activeFileEditorIsNull ) | |
| 908 | .build(); | |
| 909 | final Action editCopyAction = new ActionBuilder() | |
| 910 | .setText( Messages.get( "Main.menu.edit.copy" ) ) | |
| 911 | .setAccelerator( "Shortcut+C" ) | |
| 912 | .setIcon( COPY ) | |
| 913 | .setAction( e -> getActiveEditorPane().copy() ) | |
| 914 | .setDisable( activeFileEditorIsNull ) | |
| 915 | .build(); | |
| 916 | final Action editPasteAction = new ActionBuilder() | |
| 917 | .setText( Messages.get( "Main.menu.edit.paste" ) ) | |
| 918 | .setAccelerator( "Shortcut+V" ) | |
| 919 | .setIcon( PASTE ) | |
| 920 | .setAction( e -> getActiveEditorPane().paste() ) | |
| 921 | .setDisable( activeFileEditorIsNull ) | |
| 922 | .build(); | |
| 923 | final Action editSelectAllAction = new ActionBuilder() | |
| 924 | .setText( Messages.get( "Main.menu.edit.selectAll" ) ) | |
| 925 | .setAccelerator( "Shortcut+A" ) | |
| 926 | .setAction( e -> getActiveEditorPane().selectAll() ) | |
| 927 | .setDisable( activeFileEditorIsNull ) | |
| 928 | .build(); | |
| 929 | ||
| 930 | final Action editFindAction = new ActionBuilder() | |
| 931 | .setText( "Main.menu.edit.find" ) | |
| 932 | .setAccelerator( "Ctrl+F" ) | |
| 933 | .setIcon( SEARCH ) | |
| 934 | .setAction( e -> editFind() ) | |
| 935 | .setDisable( activeFileEditorIsNull ) | |
| 936 | .build(); | |
| 937 | final Action editFindNextAction = new ActionBuilder() | |
| 938 | .setText( "Main.menu.edit.find.next" ) | |
| 939 | .setAccelerator( "F3" ) | |
| 940 | .setIcon( null ) | |
| 941 | .setAction( e -> editFindNext() ) | |
| 942 | .setDisable( activeFileEditorIsNull ) | |
| 943 | .build(); | |
| 944 | final Action editPreferencesAction = new ActionBuilder() | |
| 945 | .setText( "Main.menu.edit.preferences" ) | |
| 946 | .setAccelerator( "Ctrl+Alt+S" ) | |
| 947 | .setAction( e -> editPreferences() ) | |
| 948 | .build(); | |
| 949 | ||
| 950 | // Insert actions | |
| 951 | final Action insertBoldAction = new ActionBuilder() | |
| 952 | .setText( "Main.menu.insert.bold" ) | |
| 953 | .setAccelerator( "Shortcut+B" ) | |
| 954 | .setIcon( BOLD ) | |
| 955 | .setAction( e -> insertMarkdown( "**", "**" ) ) | |
| 956 | .setDisable( activeFileEditorIsNull ) | |
| 957 | .build(); | |
| 958 | final Action insertItalicAction = new ActionBuilder() | |
| 959 | .setText( "Main.menu.insert.italic" ) | |
| 960 | .setAccelerator( "Shortcut+I" ) | |
| 961 | .setIcon( ITALIC ) | |
| 962 | .setAction( e -> insertMarkdown( "*", "*" ) ) | |
| 963 | .setDisable( activeFileEditorIsNull ) | |
| 964 | .build(); | |
| 965 | final Action insertSuperscriptAction = new ActionBuilder() | |
| 966 | .setText( "Main.menu.insert.superscript" ) | |
| 967 | .setAccelerator( "Shortcut+[" ) | |
| 968 | .setIcon( SUPERSCRIPT ) | |
| 969 | .setAction( e -> insertMarkdown( "^", "^" ) ) | |
| 970 | .setDisable( activeFileEditorIsNull ) | |
| 971 | .build(); | |
| 972 | final Action insertSubscriptAction = new ActionBuilder() | |
| 973 | .setText( "Main.menu.insert.subscript" ) | |
| 974 | .setAccelerator( "Shortcut+]" ) | |
| 975 | .setIcon( SUBSCRIPT ) | |
| 976 | .setAction( e -> insertMarkdown( "~", "~" ) ) | |
| 977 | .setDisable( activeFileEditorIsNull ) | |
| 978 | .build(); | |
| 979 | final Action insertStrikethroughAction = new ActionBuilder() | |
| 980 | .setText( "Main.menu.insert.strikethrough" ) | |
| 981 | .setAccelerator( "Shortcut+T" ) | |
| 982 | .setIcon( STRIKETHROUGH ) | |
| 983 | .setAction( e -> insertMarkdown( "~~", "~~" ) ) | |
| 984 | .setDisable( activeFileEditorIsNull ) | |
| 985 | .build(); | |
| 986 | final Action insertBlockquoteAction = new ActionBuilder() | |
| 987 | .setText( "Main.menu.insert.blockquote" ) | |
| 988 | .setAccelerator( "Ctrl+Q" ) | |
| 989 | .setIcon( QUOTE_LEFT ) | |
| 990 | .setAction( e -> insertMarkdown( "\n\n> ", "" ) ) | |
| 991 | .setDisable( activeFileEditorIsNull ) | |
| 992 | .build(); | |
| 993 | final Action insertCodeAction = new ActionBuilder() | |
| 994 | .setText( "Main.menu.insert.code" ) | |
| 995 | .setAccelerator( "Shortcut+K" ) | |
| 996 | .setIcon( CODE ) | |
| 997 | .setAction( e -> insertMarkdown( "`", "`" ) ) | |
| 998 | .setDisable( activeFileEditorIsNull ) | |
| 999 | .build(); | |
| 1000 | final Action insertFencedCodeBlockAction = new ActionBuilder() | |
| 1001 | .setText( "Main.menu.insert.fenced_code_block" ) | |
| 1002 | .setAccelerator( "Shortcut+Shift+K" ) | |
| 1003 | .setIcon( FILE_CODE_ALT ) | |
| 1004 | .setAction( e -> insertMarkdown( | |
| 1005 | "\n\n```\n", | |
| 1006 | "\n```\n\n", | |
| 1007 | get( "Main.menu.insert.fenced_code_block.prompt" ) ) ) | |
| 1008 | .setDisable( activeFileEditorIsNull ) | |
| 1009 | .build(); | |
| 1010 | final Action insertLinkAction = new ActionBuilder() | |
| 1011 | .setText( "Main.menu.insert.link" ) | |
| 1012 | .setAccelerator( "Shortcut+L" ) | |
| 1013 | .setIcon( LINK ) | |
| 1014 | .setAction( e -> getActiveEditorPane().insertLink() ) | |
| 1015 | .setDisable( activeFileEditorIsNull ) | |
| 1016 | .build(); | |
| 1017 | final Action insertImageAction = new ActionBuilder() | |
| 1018 | .setText( "Main.menu.insert.image" ) | |
| 1019 | .setAccelerator( "Shortcut+G" ) | |
| 1020 | .setIcon( PICTURE_ALT ) | |
| 1021 | .setAction( e -> getActiveEditorPane().insertImage() ) | |
| 1022 | .setDisable( activeFileEditorIsNull ) | |
| 1023 | .build(); | |
| 1024 | ||
| 1025 | // Number of header actions (H1 ... H3) | |
| 1026 | final int HEADERS = 3; | |
| 1027 | final Action[] headers = new Action[ HEADERS ]; | |
| 1028 | ||
| 1029 | for( int i = 1; i <= HEADERS; i++ ) { | |
| 1030 | final String hashes = new String( new char[ i ] ).replace( "\0", "#" ); | |
| 1031 | final String markup = String.format( "%n%n%s ", hashes ); | |
| 1032 | final String text = "Main.menu.insert.header." + i; | |
| 1033 | final String accelerator = "Shortcut+" + i; | |
| 1034 | final String prompt = text + ".prompt"; | |
| 1035 | ||
| 1036 | headers[ i - 1 ] = new ActionBuilder() | |
| 1037 | .setText( text ) | |
| 1038 | .setAccelerator( accelerator ) | |
| 1039 | .setIcon( HEADER ) | |
| 1040 | .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) ) | |
| 1041 | .setDisable( activeFileEditorIsNull ) | |
| 1042 | .build(); | |
| 1043 | } | |
| 1044 | ||
| 1045 | final Action insertUnorderedListAction = new ActionBuilder() | |
| 1046 | .setText( "Main.menu.insert.unordered_list" ) | |
| 1047 | .setAccelerator( "Shortcut+U" ) | |
| 1048 | .setIcon( LIST_UL ) | |
| 1049 | .setAction( e -> insertMarkdown( "\n\n* ", "" ) ) | |
| 1050 | .setDisable( activeFileEditorIsNull ) | |
| 1051 | .build(); | |
| 1052 | final Action insertOrderedListAction = new ActionBuilder() | |
| 1053 | .setText( "Main.menu.insert.ordered_list" ) | |
| 1054 | .setAccelerator( "Shortcut+Shift+O" ) | |
| 1055 | .setIcon( LIST_OL ) | |
| 1056 | .setAction( e -> insertMarkdown( | |
| 1057 | "\n\n1. ", "" ) ) | |
| 1058 | .setDisable( activeFileEditorIsNull ) | |
| 1059 | .build(); | |
| 1060 | final Action insertHorizontalRuleAction = new ActionBuilder() | |
| 1061 | .setText( "Main.menu.insert.horizontal_rule" ) | |
| 1062 | .setAccelerator( "Shortcut+H" ) | |
| 1063 | .setAction( e -> insertMarkdown( | |
| 1064 | "\n\n---\n\n", "" ) ) | |
| 1065 | .setDisable( activeFileEditorIsNull ) | |
| 1066 | .build(); | |
| 1067 | ||
| 1068 | // Help actions | |
| 1069 | final Action helpAboutAction = new ActionBuilder() | |
| 1070 | .setText( "Main.menu.help.about" ) | |
| 1071 | .setAction( e -> helpAbout() ) | |
| 1072 | .build(); | |
| 1073 | ||
| 1074 | //---- MenuBar ---- | |
| 1075 | final Menu fileMenu = ActionUtils.createMenu( | |
| 1076 | get( "Main.menu.file" ), | |
| 1077 | fileNewAction, | |
| 1078 | fileOpenAction, | |
| 1079 | null, | |
| 1080 | fileCloseAction, | |
| 1081 | fileCloseAllAction, | |
| 1082 | null, | |
| 1083 | fileSaveAction, | |
| 1084 | fileSaveAsAction, | |
| 1085 | fileSaveAllAction, | |
| 1086 | null, | |
| 1087 | fileExitAction ); | |
| 1088 | ||
| 1089 | final Menu editMenu = ActionUtils.createMenu( | |
| 1090 | get( "Main.menu.edit" ), | |
| 1091 | editCopyHtmlAction, | |
| 1092 | null, | |
| 1093 | editUndoAction, | |
| 1094 | editRedoAction, | |
| 1095 | null, | |
| 1096 | editCutAction, | |
| 1097 | editCopyAction, | |
| 1098 | editPasteAction, | |
| 1099 | null, | |
| 1100 | editFindAction, | |
| 1101 | editFindNextAction, | |
| 1102 | null, | |
| 1103 | editPreferencesAction ); | |
| 1104 | ||
| 1105 | final Menu insertMenu = ActionUtils.createMenu( | |
| 1106 | get( "Main.menu.insert" ), | |
| 1107 | insertBoldAction, | |
| 1108 | insertItalicAction, | |
| 1109 | insertSuperscriptAction, | |
| 1110 | insertSubscriptAction, | |
| 1111 | insertStrikethroughAction, | |
| 1112 | insertBlockquoteAction, | |
| 1113 | insertCodeAction, | |
| 1114 | insertFencedCodeBlockAction, | |
| 1115 | null, | |
| 1116 | insertLinkAction, | |
| 1117 | insertImageAction, | |
| 1118 | null, | |
| 1119 | headers[ 0 ], | |
| 1120 | headers[ 1 ], | |
| 1121 | headers[ 2 ], | |
| 1122 | null, | |
| 1123 | insertUnorderedListAction, | |
| 1124 | insertOrderedListAction, | |
| 1125 | insertHorizontalRuleAction | |
| 1126 | ); | |
| 1127 | ||
| 1128 | final Menu helpMenu = ActionUtils.createMenu( | |
| 1129 | get( "Main.menu.help" ), | |
| 1130 | helpAboutAction ); | |
| 1131 | ||
| 1132 | final MenuBar menuBar = new MenuBar( | |
| 1133 | fileMenu, | |
| 1134 | editMenu, | |
| 1135 | insertMenu, | |
| 1136 | helpMenu ); | |
| 1137 | ||
| 1138 | //---- ToolBar ---- | |
| 1139 | final ToolBar toolBar = ActionUtils.createToolBar( | |
| 1140 | fileNewAction, | |
| 1141 | fileOpenAction, | |
| 1142 | fileSaveAction, | |
| 1143 | null, | |
| 1144 | editUndoAction, | |
| 1145 | editRedoAction, | |
| 1146 | editCutAction, | |
| 1147 | editCopyAction, | |
| 1148 | editPasteAction, | |
| 1149 | null, | |
| 1150 | insertBoldAction, | |
| 1151 | insertItalicAction, | |
| 1152 | insertSuperscriptAction, | |
| 1153 | insertSubscriptAction, | |
| 1154 | insertBlockquoteAction, | |
| 1155 | insertCodeAction, | |
| 1156 | insertFencedCodeBlockAction, | |
| 1157 | null, | |
| 1158 | insertLinkAction, | |
| 1159 | insertImageAction, | |
| 1160 | null, | |
| 1161 | headers[ 0 ], | |
| 1162 | null, | |
| 1163 | insertUnorderedListAction, | |
| 1164 | insertOrderedListAction ); | |
| 1165 | ||
| 1166 | return new VBox( menuBar, toolBar ); | |
| 1167 | } | |
| 1168 | ||
| 1169 | /** | |
| 1170 | * Creates a boolean property that is bound to another boolean value of the | |
| 1171 | * active editor. | |
| 1172 | */ | |
| 1173 | private BooleanProperty createActiveBooleanProperty( | |
| 1174 | final Function<FileEditorTab, ObservableBooleanValue> func ) { | |
| 1175 | ||
| 1176 | final BooleanProperty b = new SimpleBooleanProperty(); | |
| 1177 | final FileEditorTab tab = getActiveFileEditorTab(); | |
| 1178 | ||
| 1179 | if( tab != null ) { | |
| 1180 | b.bind( func.apply( tab ) ); | |
| 1181 | } | |
| 1182 | ||
| 1183 | getFileEditorPane().activeFileEditorProperty().addListener( | |
| 1184 | ( observable, oldFileEditor, newFileEditor ) -> { | |
| 1185 | b.unbind(); | |
| 1186 | ||
| 1187 | if( newFileEditor == null ) { | |
| 1188 | b.set( false ); | |
| 1189 | } | |
| 1190 | else { | |
| 1191 | b.bind( func.apply( newFileEditor ) ); | |
| 1192 | } | |
| 1193 | } | |
| 1194 | ); | |
| 1195 | ||
| 1196 | return b; | |
| 1197 | } | |
| 1198 | ||
| 1199 | //---- Convenience accessors ---------------------------------------------- | |
| 1200 | ||
| 1201 | private Preferences getPreferences() { | |
| 1202 | return OPTIONS.getState(); | |
| 1203 | } | |
| 1204 | ||
| 1205 | private float getFloat( final String key, final float defaultValue ) { | |
| 1206 | return getPreferences().getFloat( key, defaultValue ); | |
| 1207 | } | |
| 1208 | ||
| 1209 | public Window getWindow() { | |
| 1210 | return getScene().getWindow(); | |
| 1211 | } | |
| 1212 | ||
| 1213 | private MarkdownEditorPane getActiveEditorPane() { | |
| 1214 | return getActiveFileEditorTab().getEditorPane(); | |
| 1215 | } | |
| 1216 | ||
| 1217 | private FileEditorTab getActiveFileEditorTab() { | |
| 1218 | return getFileEditorPane().getActiveFileEditor(); | |
| 1219 | } | |
| 1220 | ||
| 1221 | //---- Member accessors --------------------------------------------------- | |
| 1222 | ||
| 1223 | protected Scene getScene() { | |
| 1224 | return mScene; | |
| 1225 | } | |
| 1226 | ||
| 1227 | private Map<FileEditorTab, Processor<String>> getProcessors() { | |
| 1228 | return mProcessors; | |
| 1229 | } | |
| 1230 | ||
| 1231 | private FileEditorTabPane getFileEditorPane() { | |
| 1232 | return mFileEditorPane; | |
| 1233 | } | |
| 1234 | ||
| 1235 | private HTMLPreviewPane getPreviewPane() { | |
| 1236 | return mPreviewPane; | |
| 1237 | } | |
| 1238 | ||
| 1239 | private void setDefinitionSource( | |
| 1240 | final DefinitionSource definitionSource ) { | |
| 1241 | assert definitionSource != null; | |
| 1242 | mDefinitionSource = definitionSource; | |
| 1243 | } | |
| 1244 | ||
| 1245 | private DefinitionSource getDefinitionSource() { | |
| 1246 | return mDefinitionSource; | |
| 1247 | } | |
| 1248 | ||
| 1249 | private DefinitionPane getDefinitionPane() { | |
| 1250 | return mDefinitionPane; | |
| 1251 | } | |
| 1252 | ||
| 1253 | private Text getLineNumberText() { | |
| 1254 | return mLineNumberText; | |
| 1255 | } | |
| 1256 | ||
| 1257 | private StatusBar getStatusBar() { | |
| 1258 | return mStatusBar; | |
| 1259 | } | |
| 1260 | ||
| 1261 | private TextField getFindTextField() { | |
| 1262 | return mFindTextField; | |
| 1263 | } | |
| 1264 | ||
| 1265 | private VariableNameInjector getVariableNameInjector() { | |
| 1266 | return mVariableNameInjector; | |
| 1267 | } | |
| 1268 | ||
| 1269 | /** | |
| 1270 | * Returns the variable map of interpolated definitions. | |
| 1271 | * | |
| 1272 | * @return A map to help dereference variables. | |
| 1273 | */ | |
| 1274 | private Map<String, String> getResolvedMap() { | |
| 1275 | return mResolvedMap; | |
| 1276 | } | |
| 1277 | ||
| 1278 | private Notifier getNotifier() { | |
| 1279 | return NOTIFIER; | |
| 1280 | } | |
| 1281 | ||
| 1282 | //---- Persistence accessors ---------------------------------------------- | |
| 1283 | ||
| 1284 | private UserPreferences getUserPreferences() { | |
| 1285 | return OPTIONS.getUserPreferences(); | |
| 1286 | } | |
| 1287 | ||
| 1288 | private Path getDefinitionPath() { | |
| 1289 | return getUserPreferences().getDefinitionPath(); | |
| 46 | import com.scrivenvar.spelling.api.SpellChecker; | |
| 47 | import com.scrivenvar.spelling.impl.PermissiveSpeller; | |
| 48 | import com.scrivenvar.spelling.impl.SymSpellSpeller; | |
| 49 | import com.scrivenvar.util.Action; | |
| 50 | import com.scrivenvar.util.ActionBuilder; | |
| 51 | import com.scrivenvar.util.ActionUtils; | |
| 52 | import javafx.beans.binding.Bindings; | |
| 53 | import javafx.beans.binding.BooleanBinding; | |
| 54 | import javafx.beans.property.BooleanProperty; | |
| 55 | import javafx.beans.property.SimpleBooleanProperty; | |
| 56 | import javafx.beans.value.ChangeListener; | |
| 57 | import javafx.beans.value.ObservableBooleanValue; | |
| 58 | import javafx.beans.value.ObservableValue; | |
| 59 | import javafx.collections.ListChangeListener.Change; | |
| 60 | import javafx.collections.ObservableList; | |
| 61 | import javafx.event.Event; | |
| 62 | import javafx.event.EventHandler; | |
| 63 | import javafx.geometry.Pos; | |
| 64 | import javafx.scene.Node; | |
| 65 | import javafx.scene.Scene; | |
| 66 | import javafx.scene.control.*; | |
| 67 | import javafx.scene.control.Alert.AlertType; | |
| 68 | import javafx.scene.image.Image; | |
| 69 | import javafx.scene.image.ImageView; | |
| 70 | import javafx.scene.input.Clipboard; | |
| 71 | import javafx.scene.input.ClipboardContent; | |
| 72 | import javafx.scene.input.KeyEvent; | |
| 73 | import javafx.scene.layout.BorderPane; | |
| 74 | import javafx.scene.layout.VBox; | |
| 75 | import javafx.scene.text.Text; | |
| 76 | import javafx.stage.Window; | |
| 77 | import javafx.stage.WindowEvent; | |
| 78 | import javafx.util.Duration; | |
| 79 | import org.apache.commons.lang3.SystemUtils; | |
| 80 | import org.controlsfx.control.StatusBar; | |
| 81 | import org.fxmisc.richtext.StyleClassedTextArea; | |
| 82 | import org.fxmisc.richtext.model.StyleSpansBuilder; | |
| 83 | import org.reactfx.value.Val; | |
| 84 | import org.xhtmlrenderer.util.XRLog; | |
| 85 | ||
| 86 | import java.io.BufferedReader; | |
| 87 | import java.io.InputStreamReader; | |
| 88 | import java.nio.file.Path; | |
| 89 | import java.nio.file.Paths; | |
| 90 | import java.util.*; | |
| 91 | import java.util.concurrent.atomic.AtomicInteger; | |
| 92 | import java.util.function.Function; | |
| 93 | import java.util.prefs.Preferences; | |
| 94 | import java.util.stream.Collectors; | |
| 95 | ||
| 96 | import static com.scrivenvar.Constants.*; | |
| 97 | import static com.scrivenvar.Messages.get; | |
| 98 | import static com.scrivenvar.util.StageState.*; | |
| 99 | import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*; | |
| 100 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 101 | import static java.util.Collections.emptyList; | |
| 102 | import static java.util.Collections.singleton; | |
| 103 | import static javafx.application.Platform.runLater; | |
| 104 | import static javafx.event.Event.fireEvent; | |
| 105 | import static javafx.scene.input.KeyCode.ENTER; | |
| 106 | import static javafx.scene.input.KeyCode.TAB; | |
| 107 | import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST; | |
| 108 | import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward; | |
| 109 | ||
| 110 | /** | |
| 111 | * Main window containing a tab pane in the center for file editors. | |
| 112 | */ | |
| 113 | public class MainWindow implements Observer { | |
| 114 | /** | |
| 115 | * The {@code OPTIONS} variable must be declared before all other variables | |
| 116 | * to prevent subsequent initializations from failing due to missing user | |
| 117 | * preferences. | |
| 118 | */ | |
| 119 | private final static Options OPTIONS = Services.load( Options.class ); | |
| 120 | private final static Snitch SNITCH = Services.load( Snitch.class ); | |
| 121 | private final static Notifier NOTIFIER = Services.load( Notifier.class ); | |
| 122 | ||
| 123 | private final Scene mScene; | |
| 124 | private final StatusBar mStatusBar; | |
| 125 | private final Text mLineNumberText; | |
| 126 | private final TextField mFindTextField; | |
| 127 | private final SpellChecker mSpellChecker; | |
| 128 | ||
| 129 | private final Object mMutex = new Object(); | |
| 130 | ||
| 131 | /** | |
| 132 | * Prevents re-instantiation of processing classes. | |
| 133 | */ | |
| 134 | private final Map<FileEditorTab, Processor<String>> mProcessors = | |
| 135 | new HashMap<>(); | |
| 136 | ||
| 137 | private final Map<String, String> mResolvedMap = | |
| 138 | new HashMap<>( DEFAULT_MAP_SIZE ); | |
| 139 | ||
| 140 | /** | |
| 141 | * Called when the definition data is changed. | |
| 142 | */ | |
| 143 | private final EventHandler<TreeItem.TreeModificationEvent<Event>> | |
| 144 | mTreeHandler = event -> { | |
| 145 | exportDefinitions( getDefinitionPath() ); | |
| 146 | interpolateResolvedMap(); | |
| 147 | renderActiveTab(); | |
| 148 | }; | |
| 149 | ||
| 150 | /** | |
| 151 | * Called to switch to the definition pane when the user presses the TAB key. | |
| 152 | */ | |
| 153 | private final EventHandler<? super KeyEvent> mTabKeyHandler = | |
| 154 | (EventHandler<KeyEvent>) event -> { | |
| 155 | if( event.getCode() == TAB ) { | |
| 156 | getDefinitionPane().requestFocus(); | |
| 157 | event.consume(); | |
| 158 | } | |
| 159 | }; | |
| 160 | ||
| 161 | /** | |
| 162 | * Called to inject the selected item when the user presses ENTER in the | |
| 163 | * definition pane. | |
| 164 | */ | |
| 165 | private final EventHandler<? super KeyEvent> mDefinitionKeyHandler = | |
| 166 | event -> { | |
| 167 | if( event.getCode() == ENTER ) { | |
| 168 | getVariableNameInjector().injectSelectedItem(); | |
| 169 | } | |
| 170 | }; | |
| 171 | ||
| 172 | private final ChangeListener<Integer> mCaretPositionListener = | |
| 173 | ( observable, oldPosition, newPosition ) -> { | |
| 174 | final FileEditorTab tab = getActiveFileEditorTab(); | |
| 175 | final EditorPane pane = tab.getEditorPane(); | |
| 176 | final StyleClassedTextArea editor = pane.getEditor(); | |
| 177 | ||
| 178 | getLineNumberText().setText( | |
| 179 | get( STATUS_BAR_LINE, | |
| 180 | editor.getCurrentParagraph() + 1, | |
| 181 | editor.getParagraphs().size(), | |
| 182 | editor.getCaretPosition() | |
| 183 | ) | |
| 184 | ); | |
| 185 | }; | |
| 186 | ||
| 187 | private final ChangeListener<Integer> mCaretParagraphListener = | |
| 188 | ( observable, oldIndex, newIndex ) -> | |
| 189 | scrollToParagraph( newIndex, true ); | |
| 190 | ||
| 191 | private DefinitionSource mDefinitionSource = createDefaultDefinitionSource(); | |
| 192 | private final DefinitionPane mDefinitionPane = new DefinitionPane(); | |
| 193 | private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane(); | |
| 194 | private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane( | |
| 195 | mCaretPositionListener, | |
| 196 | mCaretParagraphListener ); | |
| 197 | ||
| 198 | /** | |
| 199 | * Listens on the definition pane for double-click events. | |
| 200 | */ | |
| 201 | private final VariableNameInjector mVariableNameInjector | |
| 202 | = new VariableNameInjector( mDefinitionPane ); | |
| 203 | ||
| 204 | public MainWindow() { | |
| 205 | mStatusBar = createStatusBar(); | |
| 206 | mLineNumberText = createLineNumberText(); | |
| 207 | mFindTextField = createFindTextField(); | |
| 208 | mScene = createScene(); | |
| 209 | mSpellChecker = createSpellChecker(); | |
| 210 | ||
| 211 | System.getProperties() | |
| 212 | .setProperty( "xr.util-logging.loggingEnabled", "true" ); | |
| 213 | XRLog.setLoggingEnabled( true ); | |
| 214 | ||
| 215 | initLayout(); | |
| 216 | initFindInput(); | |
| 217 | initSnitch(); | |
| 218 | initDefinitionListener(); | |
| 219 | initTabAddedListener(); | |
| 220 | initTabChangedListener(); | |
| 221 | initPreferences(); | |
| 222 | initVariableNameInjector(); | |
| 223 | ||
| 224 | NOTIFIER.addObserver( this ); | |
| 225 | } | |
| 226 | ||
| 227 | private void initLayout() { | |
| 228 | final Scene appScene = getScene(); | |
| 229 | ||
| 230 | appScene.getStylesheets().add( STYLESHEET_SCENE ); | |
| 231 | ||
| 232 | // TODO: Apply an XML syntax highlighting for XML files. | |
| 233 | // appScene.getStylesheets().add( STYLESHEET_XML ); | |
| 234 | appScene.windowProperty().addListener( | |
| 235 | ( observable, oldWindow, newWindow ) -> | |
| 236 | newWindow.setOnCloseRequest( | |
| 237 | e -> { | |
| 238 | if( !getFileEditorPane().closeAllEditors() ) { | |
| 239 | e.consume(); | |
| 240 | } | |
| 241 | } | |
| 242 | ) | |
| 243 | ); | |
| 244 | } | |
| 245 | ||
| 246 | /** | |
| 247 | * Initialize the find input text field to listen on F3, ENTER, and | |
| 248 | * ESCAPE key presses. | |
| 249 | */ | |
| 250 | private void initFindInput() { | |
| 251 | final TextField input = getFindTextField(); | |
| 252 | ||
| 253 | input.setOnKeyPressed( ( KeyEvent event ) -> { | |
| 254 | switch( event.getCode() ) { | |
| 255 | case F3: | |
| 256 | case ENTER: | |
| 257 | editFindNext(); | |
| 258 | break; | |
| 259 | case F: | |
| 260 | if( !event.isControlDown() ) { | |
| 261 | break; | |
| 262 | } | |
| 263 | case ESCAPE: | |
| 264 | getStatusBar().setGraphic( null ); | |
| 265 | getActiveFileEditorTab().getEditorPane().requestFocus(); | |
| 266 | break; | |
| 267 | } | |
| 268 | } ); | |
| 269 | ||
| 270 | // Remove when the input field loses focus. | |
| 271 | input.focusedProperty().addListener( | |
| 272 | ( focused, oldFocus, newFocus ) -> { | |
| 273 | if( !newFocus ) { | |
| 274 | getStatusBar().setGraphic( null ); | |
| 275 | } | |
| 276 | } | |
| 277 | ); | |
| 278 | } | |
| 279 | ||
| 280 | /** | |
| 281 | * Watch for changes to external files. In particular, this awaits | |
| 282 | * modifications to any XSL files associated with XML files being edited. | |
| 283 | * When | |
| 284 | * an XSL file is modified (external to the application), the snitch's ears | |
| 285 | * perk up and the file is reloaded. This keeps the XSL transformation up to | |
| 286 | * date with what's on the file system. | |
| 287 | */ | |
| 288 | private void initSnitch() { | |
| 289 | SNITCH.addObserver( this ); | |
| 290 | } | |
| 291 | ||
| 292 | /** | |
| 293 | * Listen for {@link FileEditorTabPane} to receive open definition file | |
| 294 | * event. | |
| 295 | */ | |
| 296 | private void initDefinitionListener() { | |
| 297 | getFileEditorPane().onOpenDefinitionFileProperty().addListener( | |
| 298 | ( final ObservableValue<? extends Path> file, | |
| 299 | final Path oldPath, final Path newPath ) -> { | |
| 300 | // Indirectly refresh the resolved map. | |
| 301 | resetProcessors(); | |
| 302 | ||
| 303 | openDefinitions( newPath ); | |
| 304 | ||
| 305 | // Will create new processors and therefore a new resolved map. | |
| 306 | renderActiveTab(); | |
| 307 | } | |
| 308 | ); | |
| 309 | } | |
| 310 | ||
| 311 | /** | |
| 312 | * When tabs are added, hook the various change listeners onto the new | |
| 313 | * tab sothat the preview pane refreshes as necessary. | |
| 314 | */ | |
| 315 | private void initTabAddedListener() { | |
| 316 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 317 | ||
| 318 | // Make sure the text processor kicks off when new files are opened. | |
| 319 | final ObservableList<Tab> tabs = editorPane.getTabs(); | |
| 320 | ||
| 321 | // Update the preview pane on tab changes. | |
| 322 | tabs.addListener( | |
| 323 | ( final Change<? extends Tab> change ) -> { | |
| 324 | while( change.next() ) { | |
| 325 | if( change.wasAdded() ) { | |
| 326 | // Multiple tabs can be added simultaneously. | |
| 327 | for( final Tab newTab : change.getAddedSubList() ) { | |
| 328 | final FileEditorTab tab = (FileEditorTab) newTab; | |
| 329 | ||
| 330 | initTextChangeListener( tab ); | |
| 331 | initTabKeyEventListener( tab ); | |
| 332 | initScrollEventListener( tab ); | |
| 333 | initSpellCheckListener( tab ); | |
| 334 | // initSyntaxListener( tab ); | |
| 335 | } | |
| 336 | } | |
| 337 | } | |
| 338 | } | |
| 339 | ); | |
| 340 | } | |
| 341 | ||
| 342 | private void initTextChangeListener( final FileEditorTab tab ) { | |
| 343 | tab.addTextChangeListener( | |
| 344 | ( editor, oldValue, newValue ) -> { | |
| 345 | process( tab ); | |
| 346 | scrollToParagraph( getCurrentParagraphIndex() ); | |
| 347 | } | |
| 348 | ); | |
| 349 | } | |
| 350 | ||
| 351 | /** | |
| 352 | * Ensure that the keyboard events are received when a new tab is added | |
| 353 | * to the user interface. | |
| 354 | * | |
| 355 | * @param tab The tab editor that can trigger keyboard events. | |
| 356 | */ | |
| 357 | private void initTabKeyEventListener( final FileEditorTab tab ) { | |
| 358 | tab.addEventFilter( KeyEvent.KEY_PRESSED, mTabKeyHandler ); | |
| 359 | } | |
| 360 | ||
| 361 | private void initScrollEventListener( final FileEditorTab tab ) { | |
| 362 | final var scrollPane = tab.getScrollPane(); | |
| 363 | final var scrollBar = getPreviewPane().getVerticalScrollBar(); | |
| 364 | ||
| 365 | // Before the drag handler can be attached, the scroll bar for the | |
| 366 | // text editor pane must be visible. | |
| 367 | final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) -> | |
| 368 | runLater( () -> { | |
| 369 | if( newShow ) { | |
| 370 | final var handler = new ScrollEventHandler( scrollPane, scrollBar ); | |
| 371 | handler.enabledProperty().bind( tab.selectedProperty() ); | |
| 372 | } | |
| 373 | } ); | |
| 374 | ||
| 375 | Val.flatMap( scrollPane.sceneProperty(), Scene::windowProperty ) | |
| 376 | .flatMap( Window::showingProperty ) | |
| 377 | .addListener( listener ); | |
| 378 | } | |
| 379 | ||
| 380 | /** | |
| 381 | * Listen for changes to the any particular paragraph and perform a quick | |
| 382 | * spell check upon it. The style classes in the editor will be changed to | |
| 383 | * mark any spelling mistakes in the paragraph. The user may then interact | |
| 384 | * with any misspelled word (i.e., any piece of text that is marked) to | |
| 385 | * revise the spelling. | |
| 386 | * | |
| 387 | * @param tab The tab to spellcheck. | |
| 388 | */ | |
| 389 | private void initSpellCheckListener( final FileEditorTab tab ) { | |
| 390 | final var editor = tab.getEditorPane().getEditor(); | |
| 391 | ||
| 392 | // Use the plain text changes so that notifications of style changes | |
| 393 | // are suppressed. | |
| 394 | editor.plainTextChanges() | |
| 395 | .filter( p -> !p.isIdentity() ).subscribe( change -> { | |
| 396 | ||
| 397 | // Only perform a spell check on the current paragraph. The | |
| 398 | // entire document is processed once, when opened. | |
| 399 | final var offset = change.getPosition(); | |
| 400 | final var position = editor.offsetToPosition( offset, Forward ); | |
| 401 | final var paraId = position.getMajor(); | |
| 402 | final var paragraph = editor.getParagraph( paraId ); | |
| 403 | final var text = paragraph.getText(); | |
| 404 | ||
| 405 | editor.clearStyle( paraId ); | |
| 406 | ||
| 407 | final var builder = new StyleSpansBuilder<Collection<String>>(); | |
| 408 | final var count = new AtomicInteger( 0 ); | |
| 409 | final var runningIndex = new AtomicInteger( 0 ); | |
| 410 | ||
| 411 | getSpellChecker().proofread( text, ( prevIndex, currIndex ) -> { | |
| 412 | builder.add( emptyList(), prevIndex - runningIndex.get() ); | |
| 413 | builder.add( singleton( "spelling" ), currIndex - prevIndex ); | |
| 414 | count.incrementAndGet(); | |
| 415 | runningIndex.set( currIndex ); | |
| 416 | } ); | |
| 417 | ||
| 418 | if( count.get() > 0 ) { | |
| 419 | builder.add( emptyList(), text.length() - runningIndex.get() ); | |
| 420 | ||
| 421 | final var spans = builder.create(); | |
| 422 | editor.setStyleSpans( paraId, 0, spans ); | |
| 423 | } | |
| 424 | } ); | |
| 425 | } | |
| 426 | ||
| 427 | /** | |
| 428 | * Listen for new tab selection events. | |
| 429 | */ | |
| 430 | private void initTabChangedListener() { | |
| 431 | final FileEditorTabPane editorPane = getFileEditorPane(); | |
| 432 | ||
| 433 | // Update the preview pane changing tabs. | |
| 434 | editorPane.addTabSelectionListener( | |
| 435 | ( tabPane, oldTab, newTab ) -> { | |
| 436 | if( newTab == null ) { | |
| 437 | getPreviewPane().clear(); | |
| 438 | } | |
| 439 | ||
| 440 | // If there was no old tab, then this is a first time load, which | |
| 441 | // can be ignored. | |
| 442 | if( oldTab != null ) { | |
| 443 | if( newTab != null ) { | |
| 444 | final FileEditorTab tab = (FileEditorTab) newTab; | |
| 445 | updateVariableNameInjector( tab ); | |
| 446 | process( tab ); | |
| 447 | } | |
| 448 | } | |
| 449 | } | |
| 450 | ); | |
| 451 | } | |
| 452 | ||
| 453 | /** | |
| 454 | * Reloads the preferences from the previous session. | |
| 455 | */ | |
| 456 | private void initPreferences() { | |
| 457 | initDefinitionPane(); | |
| 458 | getFileEditorPane().initPreferences(); | |
| 459 | } | |
| 460 | ||
| 461 | private void initVariableNameInjector() { | |
| 462 | updateVariableNameInjector( getActiveFileEditorTab() ); | |
| 463 | } | |
| 464 | ||
| 465 | private int getCurrentParagraphIndex() { | |
| 466 | return getActiveEditorPane().getCurrentParagraphIndex(); | |
| 467 | } | |
| 468 | ||
| 469 | private void scrollToParagraph( final int id ) { | |
| 470 | scrollToParagraph( id, false ); | |
| 471 | } | |
| 472 | ||
| 473 | /** | |
| 474 | * @param id The paragraph to scroll to, will be approximated if it doesn't | |
| 475 | * exist. | |
| 476 | * @param force {@code true} means to force scrolling immediately, which | |
| 477 | * should only be attempted when it is known that the document | |
| 478 | * has been fully rendered. Otherwise the internal map of ID | |
| 479 | * attributes will be incomplete and scrolling will flounder. | |
| 480 | */ | |
| 481 | private void scrollToParagraph( final int id, final boolean force ) { | |
| 482 | synchronized( mMutex ) { | |
| 483 | final var previewPane = getPreviewPane(); | |
| 484 | final var scrollPane = previewPane.getScrollPane(); | |
| 485 | final int approxId = getActiveEditorPane().approximateParagraphId( id ); | |
| 486 | ||
| 487 | if( force ) { | |
| 488 | previewPane.scrollTo( approxId ); | |
| 489 | } | |
| 490 | else { | |
| 491 | previewPane.tryScrollTo( approxId ); | |
| 492 | } | |
| 493 | ||
| 494 | scrollPane.repaint(); | |
| 495 | } | |
| 496 | } | |
| 497 | ||
| 498 | private void updateVariableNameInjector( final FileEditorTab tab ) { | |
| 499 | getVariableNameInjector().addListener( tab ); | |
| 500 | } | |
| 501 | ||
| 502 | /** | |
| 503 | * Called whenever the preview pane becomes out of sync with the file editor | |
| 504 | * tab. This can be called when the text changes, the caret paragraph | |
| 505 | * changes, or the file tab changes. | |
| 506 | * | |
| 507 | * @param tab The file editor tab that has been changed in some fashion. | |
| 508 | */ | |
| 509 | private void process( final FileEditorTab tab ) { | |
| 510 | if( tab == null ) { | |
| 511 | return; | |
| 512 | } | |
| 513 | ||
| 514 | getPreviewPane().setPath( tab.getPath() ); | |
| 515 | ||
| 516 | final Processor<String> processor = getProcessors().computeIfAbsent( | |
| 517 | tab, p -> createProcessors( tab ) | |
| 518 | ); | |
| 519 | ||
| 520 | try { | |
| 521 | processChain( processor, tab.getEditorText() ); | |
| 522 | } catch( final Exception ex ) { | |
| 523 | error( ex ); | |
| 524 | } | |
| 525 | } | |
| 526 | ||
| 527 | /** | |
| 528 | * Executes the processing chain, operating on the given string. | |
| 529 | * | |
| 530 | * @param handler The first processor in the chain to call. | |
| 531 | * @param text The initial value of the text to process. | |
| 532 | * @return The final value of the text that was processed by the chain. | |
| 533 | */ | |
| 534 | private String processChain( Processor<String> handler, String text ) { | |
| 535 | while( handler != null && text != null ) { | |
| 536 | text = handler.process( text ); | |
| 537 | handler = handler.next(); | |
| 538 | } | |
| 539 | ||
| 540 | return text; | |
| 541 | } | |
| 542 | ||
| 543 | private void renderActiveTab() { | |
| 544 | process( getActiveFileEditorTab() ); | |
| 545 | } | |
| 546 | ||
| 547 | /** | |
| 548 | * Called when a definition source is opened. | |
| 549 | * | |
| 550 | * @param path Path to the definition source that was opened. | |
| 551 | */ | |
| 552 | private void openDefinitions( final Path path ) { | |
| 553 | try { | |
| 554 | final DefinitionSource ds = createDefinitionSource( path ); | |
| 555 | setDefinitionSource( ds ); | |
| 556 | getUserPreferences().definitionPathProperty().setValue( path.toFile() ); | |
| 557 | getUserPreferences().save(); | |
| 558 | ||
| 559 | final Tooltip tooltipPath = new Tooltip( path.toString() ); | |
| 560 | tooltipPath.setShowDelay( Duration.millis( 200 ) ); | |
| 561 | ||
| 562 | final DefinitionPane pane = getDefinitionPane(); | |
| 563 | pane.update( ds ); | |
| 564 | pane.addTreeChangeHandler( mTreeHandler ); | |
| 565 | pane.addKeyEventHandler( mDefinitionKeyHandler ); | |
| 566 | pane.filenameProperty().setValue( path.getFileName().toString() ); | |
| 567 | pane.setTooltip( tooltipPath ); | |
| 568 | ||
| 569 | interpolateResolvedMap(); | |
| 570 | } catch( final Exception e ) { | |
| 571 | error( e ); | |
| 572 | } | |
| 573 | } | |
| 574 | ||
| 575 | private void exportDefinitions( final Path path ) { | |
| 576 | try { | |
| 577 | final DefinitionPane pane = getDefinitionPane(); | |
| 578 | final TreeItem<String> root = pane.getTreeView().getRoot(); | |
| 579 | final TreeItem<String> problemChild = pane.isTreeWellFormed(); | |
| 580 | ||
| 581 | if( problemChild == null ) { | |
| 582 | getDefinitionSource().getTreeAdapter().export( root, path ); | |
| 583 | getNotifier().clear(); | |
| 584 | } | |
| 585 | else { | |
| 586 | final String msg = get( | |
| 587 | "yaml.error.tree.form", problemChild.getValue() ); | |
| 588 | getNotifier().notify( msg ); | |
| 589 | } | |
| 590 | } catch( final Exception e ) { | |
| 591 | error( e ); | |
| 592 | } | |
| 593 | } | |
| 594 | ||
| 595 | private void interpolateResolvedMap() { | |
| 596 | final Map<String, String> treeMap = getDefinitionPane().toMap(); | |
| 597 | final Map<String, String> map = new HashMap<>( treeMap ); | |
| 598 | MapInterpolator.interpolate( map ); | |
| 599 | ||
| 600 | getResolvedMap().clear(); | |
| 601 | getResolvedMap().putAll( map ); | |
| 602 | } | |
| 603 | ||
| 604 | private void initDefinitionPane() { | |
| 605 | openDefinitions( getDefinitionPath() ); | |
| 606 | } | |
| 607 | ||
| 608 | /** | |
| 609 | * Called when an exception occurs that warrants the user's attention. | |
| 610 | * | |
| 611 | * @param e The exception with a message that the user should know about. | |
| 612 | */ | |
| 613 | private void error( final Exception e ) { | |
| 614 | getNotifier().notify( e ); | |
| 615 | } | |
| 616 | ||
| 617 | //---- File actions ------------------------------------------------------- | |
| 618 | ||
| 619 | /** | |
| 620 | * Called when an {@link Observable} instance has changed. This is called | |
| 621 | * by both the {@link Snitch} service and the notify service. The @link | |
| 622 | * Snitch} service can be called for different file types, including | |
| 623 | * {@link DefinitionSource} instances. | |
| 624 | * | |
| 625 | * @param observable The observed instance. | |
| 626 | * @param value The noteworthy item. | |
| 627 | */ | |
| 628 | @Override | |
| 629 | public void update( final Observable observable, final Object value ) { | |
| 630 | if( value != null ) { | |
| 631 | if( observable instanceof Snitch && value instanceof Path ) { | |
| 632 | updateSelectedTab(); | |
| 633 | } | |
| 634 | else if( observable instanceof Notifier && value instanceof String ) { | |
| 635 | updateStatusBar( (String) value ); | |
| 636 | } | |
| 637 | } | |
| 638 | } | |
| 639 | ||
| 640 | /** | |
| 641 | * Updates the status bar to show the given message. | |
| 642 | * | |
| 643 | * @param s The message to show in the status bar. | |
| 644 | */ | |
| 645 | private void updateStatusBar( final String s ) { | |
| 646 | runLater( | |
| 647 | () -> { | |
| 648 | final int index = s.indexOf( '\n' ); | |
| 649 | final String message = s.substring( | |
| 650 | 0, index > 0 ? index : s.length() ); | |
| 651 | ||
| 652 | getStatusBar().setText( message ); | |
| 653 | } | |
| 654 | ); | |
| 655 | } | |
| 656 | ||
| 657 | /** | |
| 658 | * Called when a file has been modified. | |
| 659 | */ | |
| 660 | private void updateSelectedTab() { | |
| 661 | runLater( | |
| 662 | () -> { | |
| 663 | // Brute-force XSLT file reload by re-instantiating all processors. | |
| 664 | resetProcessors(); | |
| 665 | renderActiveTab(); | |
| 666 | } | |
| 667 | ); | |
| 668 | } | |
| 669 | ||
| 670 | /** | |
| 671 | * After resetting the processors, they will refresh anew to be up-to-date | |
| 672 | * with the files (text and definition) currently loaded into the editor. | |
| 673 | */ | |
| 674 | private void resetProcessors() { | |
| 675 | getProcessors().clear(); | |
| 676 | } | |
| 677 | ||
| 678 | //---- File actions ------------------------------------------------------- | |
| 679 | ||
| 680 | private void fileNew() { | |
| 681 | getFileEditorPane().newEditor(); | |
| 682 | } | |
| 683 | ||
| 684 | private void fileOpen() { | |
| 685 | getFileEditorPane().openFileDialog(); | |
| 686 | } | |
| 687 | ||
| 688 | private void fileClose() { | |
| 689 | getFileEditorPane().closeEditor( getActiveFileEditorTab(), true ); | |
| 690 | } | |
| 691 | ||
| 692 | /** | |
| 693 | * TODO: Upon closing, first remove the tab change listeners. (There's no | |
| 694 | * need to re-render each tab when all are being closed.) | |
| 695 | */ | |
| 696 | private void fileCloseAll() { | |
| 697 | getFileEditorPane().closeAllEditors(); | |
| 698 | } | |
| 699 | ||
| 700 | private void fileSave() { | |
| 701 | getFileEditorPane().saveEditor( getActiveFileEditorTab() ); | |
| 702 | } | |
| 703 | ||
| 704 | private void fileSaveAs() { | |
| 705 | final FileEditorTab editor = getActiveFileEditorTab(); | |
| 706 | getFileEditorPane().saveEditorAs( editor ); | |
| 707 | getProcessors().remove( editor ); | |
| 708 | ||
| 709 | try { | |
| 710 | process( editor ); | |
| 711 | } catch( final Exception ex ) { | |
| 712 | getNotifier().notify( ex ); | |
| 713 | } | |
| 714 | } | |
| 715 | ||
| 716 | private void fileSaveAll() { | |
| 717 | getFileEditorPane().saveAllEditors(); | |
| 718 | } | |
| 719 | ||
| 720 | private void fileExit() { | |
| 721 | final Window window = getWindow(); | |
| 722 | fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) ); | |
| 723 | } | |
| 724 | ||
| 725 | //---- Edit actions ------------------------------------------------------- | |
| 726 | ||
| 727 | /** | |
| 728 | * Transform the Markdown into HTML then copy that HTML into the copy | |
| 729 | * buffer. | |
| 730 | */ | |
| 731 | private void copyHtml() { | |
| 732 | final var markdown = getActiveEditorPane().getText(); | |
| 733 | final var processors = createProcessorFactory().createProcessors( | |
| 734 | getActiveFileEditorTab() | |
| 735 | ); | |
| 736 | ||
| 737 | final var chain = processors.remove( HtmlPreviewProcessor.class ); | |
| 738 | ||
| 739 | final String html = processChain( chain, markdown ); | |
| 740 | ||
| 741 | final Clipboard clipboard = Clipboard.getSystemClipboard(); | |
| 742 | final ClipboardContent content = new ClipboardContent(); | |
| 743 | content.putString( html ); | |
| 744 | clipboard.setContent( content ); | |
| 745 | } | |
| 746 | ||
| 747 | /** | |
| 748 | * Used to find text in the active file editor window. | |
| 749 | */ | |
| 750 | private void editFind() { | |
| 751 | final TextField input = getFindTextField(); | |
| 752 | getStatusBar().setGraphic( input ); | |
| 753 | input.requestFocus(); | |
| 754 | } | |
| 755 | ||
| 756 | public void editFindNext() { | |
| 757 | getActiveFileEditorTab().searchNext( getFindTextField().getText() ); | |
| 758 | } | |
| 759 | ||
| 760 | public void editPreferences() { | |
| 761 | getUserPreferences().show(); | |
| 762 | } | |
| 763 | ||
| 764 | //---- Insert actions ----------------------------------------------------- | |
| 765 | ||
| 766 | /** | |
| 767 | * Delegates to the active editor to handle wrapping the current text | |
| 768 | * selection with leading and trailing strings. | |
| 769 | * | |
| 770 | * @param leading The string to put before the selection. | |
| 771 | * @param trailing The string to put after the selection. | |
| 772 | */ | |
| 773 | private void insertMarkdown( | |
| 774 | final String leading, final String trailing ) { | |
| 775 | getActiveEditorPane().surroundSelection( leading, trailing ); | |
| 776 | } | |
| 777 | ||
| 778 | private void insertMarkdown( | |
| 779 | final String leading, final String trailing, final String hint ) { | |
| 780 | getActiveEditorPane().surroundSelection( leading, trailing, hint ); | |
| 781 | } | |
| 782 | ||
| 783 | //---- Help actions ------------------------------------------------------- | |
| 784 | ||
| 785 | private void helpAbout() { | |
| 786 | final Alert alert = new Alert( AlertType.INFORMATION ); | |
| 787 | alert.setTitle( get( "Dialog.about.title" ) ); | |
| 788 | alert.setHeaderText( get( "Dialog.about.header" ) ); | |
| 789 | alert.setContentText( get( "Dialog.about.content" ) ); | |
| 790 | alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) ); | |
| 791 | alert.initOwner( getWindow() ); | |
| 792 | ||
| 793 | alert.showAndWait(); | |
| 794 | } | |
| 795 | ||
| 796 | //---- Member creators ---------------------------------------------------- | |
| 797 | ||
| 798 | private SpellChecker createSpellChecker() { | |
| 799 | try { | |
| 800 | final Collection<String> lexicon = readLexicon( "en.txt" ); | |
| 801 | return SymSpellSpeller.forLexicon( lexicon ); | |
| 802 | } catch( final Exception e ) { | |
| 803 | getNotifier().notify( e ); | |
| 804 | return new PermissiveSpeller(); | |
| 805 | } | |
| 806 | } | |
| 807 | ||
| 808 | /** | |
| 809 | * Factory to create processors that are suited to different file types. | |
| 810 | * | |
| 811 | * @param tab The tab that is subjected to processing. | |
| 812 | * @return A processor suited to the file type specified by the tab's path. | |
| 813 | */ | |
| 814 | private Processor<String> createProcessors( final FileEditorTab tab ) { | |
| 815 | return createProcessorFactory().createProcessors( tab ); | |
| 816 | } | |
| 817 | ||
| 818 | private ProcessorFactory createProcessorFactory() { | |
| 819 | return new ProcessorFactory( getPreviewPane(), getResolvedMap() ); | |
| 820 | } | |
| 821 | ||
| 822 | private HTMLPreviewPane createHTMLPreviewPane() { | |
| 823 | return new HTMLPreviewPane(); | |
| 824 | } | |
| 825 | ||
| 826 | private DefinitionSource createDefaultDefinitionSource() { | |
| 827 | return new YamlDefinitionSource( getDefinitionPath() ); | |
| 828 | } | |
| 829 | ||
| 830 | private DefinitionSource createDefinitionSource( final Path path ) { | |
| 831 | try { | |
| 832 | return createDefinitionFactory().createDefinitionSource( path ); | |
| 833 | } catch( final Exception ex ) { | |
| 834 | error( ex ); | |
| 835 | return createDefaultDefinitionSource(); | |
| 836 | } | |
| 837 | } | |
| 838 | ||
| 839 | private TextField createFindTextField() { | |
| 840 | return new TextField(); | |
| 841 | } | |
| 842 | ||
| 843 | private DefinitionFactory createDefinitionFactory() { | |
| 844 | return new DefinitionFactory(); | |
| 845 | } | |
| 846 | ||
| 847 | private StatusBar createStatusBar() { | |
| 848 | return new StatusBar(); | |
| 849 | } | |
| 850 | ||
| 851 | private Scene createScene() { | |
| 852 | final SplitPane splitPane = new SplitPane( | |
| 853 | getDefinitionPane().getNode(), | |
| 854 | getFileEditorPane().getNode(), | |
| 855 | getPreviewPane().getNode() ); | |
| 856 | ||
| 857 | splitPane.setDividerPositions( | |
| 858 | getFloat( K_PANE_SPLIT_DEFINITION, .22f ), | |
| 859 | getFloat( K_PANE_SPLIT_EDITOR, .60f ), | |
| 860 | getFloat( K_PANE_SPLIT_PREVIEW, .18f ) ); | |
| 861 | ||
| 862 | getDefinitionPane().prefHeightProperty() | |
| 863 | .bind( splitPane.heightProperty() ); | |
| 864 | ||
| 865 | final BorderPane borderPane = new BorderPane(); | |
| 866 | borderPane.setPrefSize( 1280, 800 ); | |
| 867 | borderPane.setTop( createMenuBar() ); | |
| 868 | borderPane.setBottom( getStatusBar() ); | |
| 869 | borderPane.setCenter( splitPane ); | |
| 870 | ||
| 871 | final VBox statusBar = new VBox(); | |
| 872 | statusBar.setAlignment( Pos.BASELINE_CENTER ); | |
| 873 | statusBar.getChildren().add( getLineNumberText() ); | |
| 874 | getStatusBar().getRightItems().add( statusBar ); | |
| 875 | ||
| 876 | // Force preview pane refresh on Windows. | |
| 877 | if( SystemUtils.IS_OS_WINDOWS ) { | |
| 878 | splitPane.getDividers().get( 1 ).positionProperty().addListener( | |
| 879 | ( l, oValue, nValue ) -> runLater( | |
| 880 | () -> getPreviewPane().getScrollPane().repaint() | |
| 881 | ) | |
| 882 | ); | |
| 883 | } | |
| 884 | ||
| 885 | return new Scene( borderPane ); | |
| 886 | } | |
| 887 | ||
| 888 | private Text createLineNumberText() { | |
| 889 | return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) ); | |
| 890 | } | |
| 891 | ||
| 892 | private Node createMenuBar() { | |
| 893 | final BooleanBinding activeFileEditorIsNull = | |
| 894 | getFileEditorPane().activeFileEditorProperty().isNull(); | |
| 895 | ||
| 896 | // File actions | |
| 897 | final Action fileNewAction = new ActionBuilder() | |
| 898 | .setText( "Main.menu.file.new" ) | |
| 899 | .setAccelerator( "Shortcut+N" ) | |
| 900 | .setIcon( FILE_ALT ) | |
| 901 | .setAction( e -> fileNew() ) | |
| 902 | .build(); | |
| 903 | final Action fileOpenAction = new ActionBuilder() | |
| 904 | .setText( "Main.menu.file.open" ) | |
| 905 | .setAccelerator( "Shortcut+O" ) | |
| 906 | .setIcon( FOLDER_OPEN_ALT ) | |
| 907 | .setAction( e -> fileOpen() ) | |
| 908 | .build(); | |
| 909 | final Action fileCloseAction = new ActionBuilder() | |
| 910 | .setText( "Main.menu.file.close" ) | |
| 911 | .setAccelerator( "Shortcut+W" ) | |
| 912 | .setAction( e -> fileClose() ) | |
| 913 | .setDisable( activeFileEditorIsNull ) | |
| 914 | .build(); | |
| 915 | final Action fileCloseAllAction = new ActionBuilder() | |
| 916 | .setText( "Main.menu.file.close_all" ) | |
| 917 | .setAction( e -> fileCloseAll() ) | |
| 918 | .setDisable( activeFileEditorIsNull ) | |
| 919 | .build(); | |
| 920 | final Action fileSaveAction = new ActionBuilder() | |
| 921 | .setText( "Main.menu.file.save" ) | |
| 922 | .setAccelerator( "Shortcut+S" ) | |
| 923 | .setIcon( FLOPPY_ALT ) | |
| 924 | .setAction( e -> fileSave() ) | |
| 925 | .setDisable( createActiveBooleanProperty( | |
| 926 | FileEditorTab::modifiedProperty ).not() ) | |
| 927 | .build(); | |
| 928 | final Action fileSaveAsAction = new ActionBuilder() | |
| 929 | .setText( "Main.menu.file.save_as" ) | |
| 930 | .setAction( e -> fileSaveAs() ) | |
| 931 | .setDisable( activeFileEditorIsNull ) | |
| 932 | .build(); | |
| 933 | final Action fileSaveAllAction = new ActionBuilder() | |
| 934 | .setText( "Main.menu.file.save_all" ) | |
| 935 | .setAccelerator( "Shortcut+Shift+S" ) | |
| 936 | .setAction( e -> fileSaveAll() ) | |
| 937 | .setDisable( Bindings.not( | |
| 938 | getFileEditorPane().anyFileEditorModifiedProperty() ) ) | |
| 939 | .build(); | |
| 940 | final Action fileExitAction = new ActionBuilder() | |
| 941 | .setText( "Main.menu.file.exit" ) | |
| 942 | .setAction( e -> fileExit() ) | |
| 943 | .build(); | |
| 944 | ||
| 945 | // Edit actions | |
| 946 | final Action editCopyHtmlAction = new ActionBuilder() | |
| 947 | .setText( Messages.get( "Main.menu.edit.copy.html" ) ) | |
| 948 | .setIcon( HTML5 ) | |
| 949 | .setAction( e -> copyHtml() ) | |
| 950 | .setDisable( activeFileEditorIsNull ) | |
| 951 | .build(); | |
| 952 | ||
| 953 | final Action editUndoAction = new ActionBuilder() | |
| 954 | .setText( "Main.menu.edit.undo" ) | |
| 955 | .setAccelerator( "Shortcut+Z" ) | |
| 956 | .setIcon( UNDO ) | |
| 957 | .setAction( e -> getActiveEditorPane().undo() ) | |
| 958 | .setDisable( createActiveBooleanProperty( | |
| 959 | FileEditorTab::canUndoProperty ).not() ) | |
| 960 | .build(); | |
| 961 | final Action editRedoAction = new ActionBuilder() | |
| 962 | .setText( "Main.menu.edit.redo" ) | |
| 963 | .setAccelerator( "Shortcut+Y" ) | |
| 964 | .setIcon( REPEAT ) | |
| 965 | .setAction( e -> getActiveEditorPane().redo() ) | |
| 966 | .setDisable( createActiveBooleanProperty( | |
| 967 | FileEditorTab::canRedoProperty ).not() ) | |
| 968 | .build(); | |
| 969 | ||
| 970 | final Action editCutAction = new ActionBuilder() | |
| 971 | .setText( Messages.get( "Main.menu.edit.cut" ) ) | |
| 972 | .setAccelerator( "Shortcut+X" ) | |
| 973 | .setIcon( CUT ) | |
| 974 | .setAction( e -> getActiveEditorPane().cut() ) | |
| 975 | .setDisable( activeFileEditorIsNull ) | |
| 976 | .build(); | |
| 977 | final Action editCopyAction = new ActionBuilder() | |
| 978 | .setText( Messages.get( "Main.menu.edit.copy" ) ) | |
| 979 | .setAccelerator( "Shortcut+C" ) | |
| 980 | .setIcon( COPY ) | |
| 981 | .setAction( e -> getActiveEditorPane().copy() ) | |
| 982 | .setDisable( activeFileEditorIsNull ) | |
| 983 | .build(); | |
| 984 | final Action editPasteAction = new ActionBuilder() | |
| 985 | .setText( Messages.get( "Main.menu.edit.paste" ) ) | |
| 986 | .setAccelerator( "Shortcut+V" ) | |
| 987 | .setIcon( PASTE ) | |
| 988 | .setAction( e -> getActiveEditorPane().paste() ) | |
| 989 | .setDisable( activeFileEditorIsNull ) | |
| 990 | .build(); | |
| 991 | final Action editSelectAllAction = new ActionBuilder() | |
| 992 | .setText( Messages.get( "Main.menu.edit.selectAll" ) ) | |
| 993 | .setAccelerator( "Shortcut+A" ) | |
| 994 | .setAction( e -> getActiveEditorPane().selectAll() ) | |
| 995 | .setDisable( activeFileEditorIsNull ) | |
| 996 | .build(); | |
| 997 | ||
| 998 | final Action editFindAction = new ActionBuilder() | |
| 999 | .setText( "Main.menu.edit.find" ) | |
| 1000 | .setAccelerator( "Ctrl+F" ) | |
| 1001 | .setIcon( SEARCH ) | |
| 1002 | .setAction( e -> editFind() ) | |
| 1003 | .setDisable( activeFileEditorIsNull ) | |
| 1004 | .build(); | |
| 1005 | final Action editFindNextAction = new ActionBuilder() | |
| 1006 | .setText( "Main.menu.edit.find.next" ) | |
| 1007 | .setAccelerator( "F3" ) | |
| 1008 | .setIcon( null ) | |
| 1009 | .setAction( e -> editFindNext() ) | |
| 1010 | .setDisable( activeFileEditorIsNull ) | |
| 1011 | .build(); | |
| 1012 | final Action editPreferencesAction = new ActionBuilder() | |
| 1013 | .setText( "Main.menu.edit.preferences" ) | |
| 1014 | .setAccelerator( "Ctrl+Alt+S" ) | |
| 1015 | .setAction( e -> editPreferences() ) | |
| 1016 | .build(); | |
| 1017 | ||
| 1018 | // Insert actions | |
| 1019 | final Action insertBoldAction = new ActionBuilder() | |
| 1020 | .setText( "Main.menu.insert.bold" ) | |
| 1021 | .setAccelerator( "Shortcut+B" ) | |
| 1022 | .setIcon( BOLD ) | |
| 1023 | .setAction( e -> insertMarkdown( "**", "**" ) ) | |
| 1024 | .setDisable( activeFileEditorIsNull ) | |
| 1025 | .build(); | |
| 1026 | final Action insertItalicAction = new ActionBuilder() | |
| 1027 | .setText( "Main.menu.insert.italic" ) | |
| 1028 | .setAccelerator( "Shortcut+I" ) | |
| 1029 | .setIcon( ITALIC ) | |
| 1030 | .setAction( e -> insertMarkdown( "*", "*" ) ) | |
| 1031 | .setDisable( activeFileEditorIsNull ) | |
| 1032 | .build(); | |
| 1033 | final Action insertSuperscriptAction = new ActionBuilder() | |
| 1034 | .setText( "Main.menu.insert.superscript" ) | |
| 1035 | .setAccelerator( "Shortcut+[" ) | |
| 1036 | .setIcon( SUPERSCRIPT ) | |
| 1037 | .setAction( e -> insertMarkdown( "^", "^" ) ) | |
| 1038 | .setDisable( activeFileEditorIsNull ) | |
| 1039 | .build(); | |
| 1040 | final Action insertSubscriptAction = new ActionBuilder() | |
| 1041 | .setText( "Main.menu.insert.subscript" ) | |
| 1042 | .setAccelerator( "Shortcut+]" ) | |
| 1043 | .setIcon( SUBSCRIPT ) | |
| 1044 | .setAction( e -> insertMarkdown( "~", "~" ) ) | |
| 1045 | .setDisable( activeFileEditorIsNull ) | |
| 1046 | .build(); | |
| 1047 | final Action insertStrikethroughAction = new ActionBuilder() | |
| 1048 | .setText( "Main.menu.insert.strikethrough" ) | |
| 1049 | .setAccelerator( "Shortcut+T" ) | |
| 1050 | .setIcon( STRIKETHROUGH ) | |
| 1051 | .setAction( e -> insertMarkdown( "~~", "~~" ) ) | |
| 1052 | .setDisable( activeFileEditorIsNull ) | |
| 1053 | .build(); | |
| 1054 | final Action insertBlockquoteAction = new ActionBuilder() | |
| 1055 | .setText( "Main.menu.insert.blockquote" ) | |
| 1056 | .setAccelerator( "Ctrl+Q" ) | |
| 1057 | .setIcon( QUOTE_LEFT ) | |
| 1058 | .setAction( e -> insertMarkdown( "\n\n> ", "" ) ) | |
| 1059 | .setDisable( activeFileEditorIsNull ) | |
| 1060 | .build(); | |
| 1061 | final Action insertCodeAction = new ActionBuilder() | |
| 1062 | .setText( "Main.menu.insert.code" ) | |
| 1063 | .setAccelerator( "Shortcut+K" ) | |
| 1064 | .setIcon( CODE ) | |
| 1065 | .setAction( e -> insertMarkdown( "`", "`" ) ) | |
| 1066 | .setDisable( activeFileEditorIsNull ) | |
| 1067 | .build(); | |
| 1068 | final Action insertFencedCodeBlockAction = new ActionBuilder() | |
| 1069 | .setText( "Main.menu.insert.fenced_code_block" ) | |
| 1070 | .setAccelerator( "Shortcut+Shift+K" ) | |
| 1071 | .setIcon( FILE_CODE_ALT ) | |
| 1072 | .setAction( e -> insertMarkdown( | |
| 1073 | "\n\n```\n", | |
| 1074 | "\n```\n\n", | |
| 1075 | get( "Main.menu.insert.fenced_code_block.prompt" ) ) ) | |
| 1076 | .setDisable( activeFileEditorIsNull ) | |
| 1077 | .build(); | |
| 1078 | final Action insertLinkAction = new ActionBuilder() | |
| 1079 | .setText( "Main.menu.insert.link" ) | |
| 1080 | .setAccelerator( "Shortcut+L" ) | |
| 1081 | .setIcon( LINK ) | |
| 1082 | .setAction( e -> getActiveEditorPane().insertLink() ) | |
| 1083 | .setDisable( activeFileEditorIsNull ) | |
| 1084 | .build(); | |
| 1085 | final Action insertImageAction = new ActionBuilder() | |
| 1086 | .setText( "Main.menu.insert.image" ) | |
| 1087 | .setAccelerator( "Shortcut+G" ) | |
| 1088 | .setIcon( PICTURE_ALT ) | |
| 1089 | .setAction( e -> getActiveEditorPane().insertImage() ) | |
| 1090 | .setDisable( activeFileEditorIsNull ) | |
| 1091 | .build(); | |
| 1092 | ||
| 1093 | // Number of header actions (H1 ... H3) | |
| 1094 | final int HEADERS = 3; | |
| 1095 | final Action[] headers = new Action[ HEADERS ]; | |
| 1096 | ||
| 1097 | for( int i = 1; i <= HEADERS; i++ ) { | |
| 1098 | final String hashes = new String( new char[ i ] ).replace( "\0", "#" ); | |
| 1099 | final String markup = String.format( "%n%n%s ", hashes ); | |
| 1100 | final String text = "Main.menu.insert.header." + i; | |
| 1101 | final String accelerator = "Shortcut+" + i; | |
| 1102 | final String prompt = text + ".prompt"; | |
| 1103 | ||
| 1104 | headers[ i - 1 ] = new ActionBuilder() | |
| 1105 | .setText( text ) | |
| 1106 | .setAccelerator( accelerator ) | |
| 1107 | .setIcon( HEADER ) | |
| 1108 | .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) ) | |
| 1109 | .setDisable( activeFileEditorIsNull ) | |
| 1110 | .build(); | |
| 1111 | } | |
| 1112 | ||
| 1113 | final Action insertUnorderedListAction = new ActionBuilder() | |
| 1114 | .setText( "Main.menu.insert.unordered_list" ) | |
| 1115 | .setAccelerator( "Shortcut+U" ) | |
| 1116 | .setIcon( LIST_UL ) | |
| 1117 | .setAction( e -> insertMarkdown( "\n\n* ", "" ) ) | |
| 1118 | .setDisable( activeFileEditorIsNull ) | |
| 1119 | .build(); | |
| 1120 | final Action insertOrderedListAction = new ActionBuilder() | |
| 1121 | .setText( "Main.menu.insert.ordered_list" ) | |
| 1122 | .setAccelerator( "Shortcut+Shift+O" ) | |
| 1123 | .setIcon( LIST_OL ) | |
| 1124 | .setAction( e -> insertMarkdown( | |
| 1125 | "\n\n1. ", "" ) ) | |
| 1126 | .setDisable( activeFileEditorIsNull ) | |
| 1127 | .build(); | |
| 1128 | final Action insertHorizontalRuleAction = new ActionBuilder() | |
| 1129 | .setText( "Main.menu.insert.horizontal_rule" ) | |
| 1130 | .setAccelerator( "Shortcut+H" ) | |
| 1131 | .setAction( e -> insertMarkdown( | |
| 1132 | "\n\n---\n\n", "" ) ) | |
| 1133 | .setDisable( activeFileEditorIsNull ) | |
| 1134 | .build(); | |
| 1135 | ||
| 1136 | // Help actions | |
| 1137 | final Action helpAboutAction = new ActionBuilder() | |
| 1138 | .setText( "Main.menu.help.about" ) | |
| 1139 | .setAction( e -> helpAbout() ) | |
| 1140 | .build(); | |
| 1141 | ||
| 1142 | //---- MenuBar ---- | |
| 1143 | final Menu fileMenu = ActionUtils.createMenu( | |
| 1144 | get( "Main.menu.file" ), | |
| 1145 | fileNewAction, | |
| 1146 | fileOpenAction, | |
| 1147 | null, | |
| 1148 | fileCloseAction, | |
| 1149 | fileCloseAllAction, | |
| 1150 | null, | |
| 1151 | fileSaveAction, | |
| 1152 | fileSaveAsAction, | |
| 1153 | fileSaveAllAction, | |
| 1154 | null, | |
| 1155 | fileExitAction ); | |
| 1156 | ||
| 1157 | final Menu editMenu = ActionUtils.createMenu( | |
| 1158 | get( "Main.menu.edit" ), | |
| 1159 | editCopyHtmlAction, | |
| 1160 | null, | |
| 1161 | editUndoAction, | |
| 1162 | editRedoAction, | |
| 1163 | null, | |
| 1164 | editCutAction, | |
| 1165 | editCopyAction, | |
| 1166 | editPasteAction, | |
| 1167 | editSelectAllAction, | |
| 1168 | null, | |
| 1169 | editFindAction, | |
| 1170 | editFindNextAction, | |
| 1171 | null, | |
| 1172 | editPreferencesAction ); | |
| 1173 | ||
| 1174 | final Menu insertMenu = ActionUtils.createMenu( | |
| 1175 | get( "Main.menu.insert" ), | |
| 1176 | insertBoldAction, | |
| 1177 | insertItalicAction, | |
| 1178 | insertSuperscriptAction, | |
| 1179 | insertSubscriptAction, | |
| 1180 | insertStrikethroughAction, | |
| 1181 | insertBlockquoteAction, | |
| 1182 | insertCodeAction, | |
| 1183 | insertFencedCodeBlockAction, | |
| 1184 | null, | |
| 1185 | insertLinkAction, | |
| 1186 | insertImageAction, | |
| 1187 | null, | |
| 1188 | headers[ 0 ], | |
| 1189 | headers[ 1 ], | |
| 1190 | headers[ 2 ], | |
| 1191 | null, | |
| 1192 | insertUnorderedListAction, | |
| 1193 | insertOrderedListAction, | |
| 1194 | insertHorizontalRuleAction | |
| 1195 | ); | |
| 1196 | ||
| 1197 | final Menu helpMenu = ActionUtils.createMenu( | |
| 1198 | get( "Main.menu.help" ), | |
| 1199 | helpAboutAction ); | |
| 1200 | ||
| 1201 | final MenuBar menuBar = new MenuBar( | |
| 1202 | fileMenu, | |
| 1203 | editMenu, | |
| 1204 | insertMenu, | |
| 1205 | helpMenu ); | |
| 1206 | ||
| 1207 | //---- ToolBar ---- | |
| 1208 | final ToolBar toolBar = ActionUtils.createToolBar( | |
| 1209 | fileNewAction, | |
| 1210 | fileOpenAction, | |
| 1211 | fileSaveAction, | |
| 1212 | null, | |
| 1213 | editUndoAction, | |
| 1214 | editRedoAction, | |
| 1215 | editCutAction, | |
| 1216 | editCopyAction, | |
| 1217 | editPasteAction, | |
| 1218 | null, | |
| 1219 | insertBoldAction, | |
| 1220 | insertItalicAction, | |
| 1221 | insertSuperscriptAction, | |
| 1222 | insertSubscriptAction, | |
| 1223 | insertBlockquoteAction, | |
| 1224 | insertCodeAction, | |
| 1225 | insertFencedCodeBlockAction, | |
| 1226 | null, | |
| 1227 | insertLinkAction, | |
| 1228 | insertImageAction, | |
| 1229 | null, | |
| 1230 | headers[ 0 ], | |
| 1231 | null, | |
| 1232 | insertUnorderedListAction, | |
| 1233 | insertOrderedListAction ); | |
| 1234 | ||
| 1235 | return new VBox( menuBar, toolBar ); | |
| 1236 | } | |
| 1237 | ||
| 1238 | /** | |
| 1239 | * Creates a boolean property that is bound to another boolean value of the | |
| 1240 | * active editor. | |
| 1241 | */ | |
| 1242 | private BooleanProperty createActiveBooleanProperty( | |
| 1243 | final Function<FileEditorTab, ObservableBooleanValue> func ) { | |
| 1244 | ||
| 1245 | final BooleanProperty b = new SimpleBooleanProperty(); | |
| 1246 | final FileEditorTab tab = getActiveFileEditorTab(); | |
| 1247 | ||
| 1248 | if( tab != null ) { | |
| 1249 | b.bind( func.apply( tab ) ); | |
| 1250 | } | |
| 1251 | ||
| 1252 | getFileEditorPane().activeFileEditorProperty().addListener( | |
| 1253 | ( observable, oldFileEditor, newFileEditor ) -> { | |
| 1254 | b.unbind(); | |
| 1255 | ||
| 1256 | if( newFileEditor == null ) { | |
| 1257 | b.set( false ); | |
| 1258 | } | |
| 1259 | else { | |
| 1260 | b.bind( func.apply( newFileEditor ) ); | |
| 1261 | } | |
| 1262 | } | |
| 1263 | ); | |
| 1264 | ||
| 1265 | return b; | |
| 1266 | } | |
| 1267 | ||
| 1268 | //---- Convenience accessors ---------------------------------------------- | |
| 1269 | ||
| 1270 | private Preferences getPreferences() { | |
| 1271 | return OPTIONS.getState(); | |
| 1272 | } | |
| 1273 | ||
| 1274 | private float getFloat( final String key, final float defaultValue ) { | |
| 1275 | return getPreferences().getFloat( key, defaultValue ); | |
| 1276 | } | |
| 1277 | ||
| 1278 | public Window getWindow() { | |
| 1279 | return getScene().getWindow(); | |
| 1280 | } | |
| 1281 | ||
| 1282 | private MarkdownEditorPane getActiveEditorPane() { | |
| 1283 | return getActiveFileEditorTab().getEditorPane(); | |
| 1284 | } | |
| 1285 | ||
| 1286 | private FileEditorTab getActiveFileEditorTab() { | |
| 1287 | return getFileEditorPane().getActiveFileEditor(); | |
| 1288 | } | |
| 1289 | ||
| 1290 | //---- Member accessors --------------------------------------------------- | |
| 1291 | ||
| 1292 | protected Scene getScene() { | |
| 1293 | return mScene; | |
| 1294 | } | |
| 1295 | ||
| 1296 | private SpellChecker getSpellChecker() { | |
| 1297 | return mSpellChecker; | |
| 1298 | } | |
| 1299 | ||
| 1300 | private Map<FileEditorTab, Processor<String>> getProcessors() { | |
| 1301 | return mProcessors; | |
| 1302 | } | |
| 1303 | ||
| 1304 | private FileEditorTabPane getFileEditorPane() { | |
| 1305 | return mFileEditorPane; | |
| 1306 | } | |
| 1307 | ||
| 1308 | private HTMLPreviewPane getPreviewPane() { | |
| 1309 | return mPreviewPane; | |
| 1310 | } | |
| 1311 | ||
| 1312 | private void setDefinitionSource( | |
| 1313 | final DefinitionSource definitionSource ) { | |
| 1314 | assert definitionSource != null; | |
| 1315 | mDefinitionSource = definitionSource; | |
| 1316 | } | |
| 1317 | ||
| 1318 | private DefinitionSource getDefinitionSource() { | |
| 1319 | return mDefinitionSource; | |
| 1320 | } | |
| 1321 | ||
| 1322 | private DefinitionPane getDefinitionPane() { | |
| 1323 | return mDefinitionPane; | |
| 1324 | } | |
| 1325 | ||
| 1326 | private Text getLineNumberText() { | |
| 1327 | return mLineNumberText; | |
| 1328 | } | |
| 1329 | ||
| 1330 | private StatusBar getStatusBar() { | |
| 1331 | return mStatusBar; | |
| 1332 | } | |
| 1333 | ||
| 1334 | private TextField getFindTextField() { | |
| 1335 | return mFindTextField; | |
| 1336 | } | |
| 1337 | ||
| 1338 | private VariableNameInjector getVariableNameInjector() { | |
| 1339 | return mVariableNameInjector; | |
| 1340 | } | |
| 1341 | ||
| 1342 | /** | |
| 1343 | * Returns the variable map of interpolated definitions. | |
| 1344 | * | |
| 1345 | * @return A map to help dereference variables. | |
| 1346 | */ | |
| 1347 | private Map<String, String> getResolvedMap() { | |
| 1348 | return mResolvedMap; | |
| 1349 | } | |
| 1350 | ||
| 1351 | private Notifier getNotifier() { | |
| 1352 | return NOTIFIER; | |
| 1353 | } | |
| 1354 | ||
| 1355 | //---- Persistence accessors ---------------------------------------------- | |
| 1356 | ||
| 1357 | private UserPreferences getUserPreferences() { | |
| 1358 | return OPTIONS.getUserPreferences(); | |
| 1359 | } | |
| 1360 | ||
| 1361 | private Path getDefinitionPath() { | |
| 1362 | return getUserPreferences().getDefinitionPath(); | |
| 1363 | } | |
| 1364 | ||
| 1365 | //---- Resource accessors ------------------------------------------------- | |
| 1366 | ||
| 1367 | @SuppressWarnings("SameParameterValue") | |
| 1368 | private Collection<String> readLexicon( final String filename ) | |
| 1369 | throws Exception { | |
| 1370 | final var path = Paths.get( LEXICONS_DIRECTORY, filename ).toString(); | |
| 1371 | final var classLoader = MainWindow.class.getClassLoader(); | |
| 1372 | ||
| 1373 | try( final var resource = classLoader.getResourceAsStream( path ) ) { | |
| 1374 | assert resource != null; | |
| 1375 | ||
| 1376 | return new BufferedReader( new InputStreamReader( resource, UTF_8 ) ) | |
| 1377 | .lines() | |
| 1378 | .collect( Collectors.toList() ); | |
| 1379 | } | |
| 1290 | 1380 | } |
| 1291 | 1381 | } |
| 47 | 47 | |
| 48 | 48 | /** |
| 49 | * Button that opens a file chooser to select a local file for a URL in | |
| 50 | * markdown. | |
| 49 | * Button that opens a file chooser to select a local file for a URL. | |
| 51 | 50 | */ |
| 52 | 51 | public class BrowseFileButton extends Button { |
| 34 | 34 | |
| 35 | 35 | /** |
| 36 | * TextField that can escape/unescape characters for markdown. | |
| 36 | * Responsible for escaping/unescaping characters for markdown. | |
| 37 | 37 | */ |
| 38 | 38 | public class EscapeTextField extends TextField { |
| 29 | 29 | |
| 30 | 30 | /** |
| 31 | * Brackets variable names with <code>`r#</code> and <code>`</code>. | |
| 31 | * Brackets variable names with {@code `r#} and {@code `}. | |
| 32 | 32 | */ |
| 33 | 33 | public class RVariableDecorator implements VariableDecorator { |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 1 | 28 | package com.scrivenvar.definition; |
| 2 | 29 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.dialogs; | |
| 29 | ||
| 30 | import javafx.application.Platform; | |
| 31 | import javafx.geometry.Insets; | |
| 32 | import javafx.scene.control.Label; | |
| 33 | import javafx.scene.control.TextArea; | |
| 34 | import javafx.scene.layout.GridPane; | |
| 35 | import javafx.stage.Window; | |
| 36 | ||
| 37 | import static com.scrivenvar.Messages.get; | |
| 38 | import static javafx.scene.control.ButtonType.OK; | |
| 39 | ||
| 40 | /** | |
| 41 | * Responsible for managing the R startup script that is run when an R source | |
| 42 | * file is loaded. | |
| 43 | */ | |
| 44 | public class RScriptDialog extends AbstractDialog<String> { | |
| 45 | ||
| 46 | private TextArea mScriptArea; | |
| 47 | private final String mOriginalText; | |
| 48 | ||
| 49 | public RScriptDialog( | |
| 50 | final Window parent, final String title, final String script ) { | |
| 51 | super( parent, title ); | |
| 52 | mOriginalText = script; | |
| 53 | getScriptArea().setText( script ); | |
| 54 | } | |
| 55 | ||
| 56 | @Override | |
| 57 | protected void initComponents() { | |
| 58 | final GridPane grid = new GridPane(); | |
| 59 | grid.setHgap( 10 ); | |
| 60 | grid.setVgap( 10 ); | |
| 61 | grid.setPadding( new Insets( 10, 10, 10, 10 ) ); | |
| 62 | ||
| 63 | final Label label = new Label( get( "Dialog.r.script.content" ) ); | |
| 64 | ||
| 65 | final TextArea textArea = getScriptArea(); | |
| 66 | textArea.setEditable( true ); | |
| 67 | textArea.setWrapText( true ); | |
| 68 | ||
| 69 | grid.add( label, 0, 0 ); | |
| 70 | grid.add( textArea, 0, 1 ); | |
| 71 | ||
| 72 | getDialogPane().setContent( grid ); | |
| 73 | ||
| 74 | Platform.runLater( textArea::requestFocus ); | |
| 75 | ||
| 76 | setResultConverter( | |
| 77 | dialogButton -> dialogButton == OK ? | |
| 78 | textArea.getText() : | |
| 79 | getOriginalText() | |
| 80 | ); | |
| 81 | } | |
| 82 | ||
| 83 | private TextArea getScriptArea() { | |
| 84 | if( mScriptArea == null ) { | |
| 85 | mScriptArea = new TextArea(); | |
| 86 | } | |
| 87 | ||
| 88 | return mScriptArea; | |
| 89 | } | |
| 90 | ||
| 91 | private String getOriginalText() { | |
| 92 | return mOriginalText; | |
| 93 | } | |
| 94 | } | |
| 95 | 1 |
| 31 | 31 | |
| 32 | 32 | /** |
| 33 | * Represents the model for a hyperlink: text and url text. | |
| 33 | * Represents the model for a hyperlink: text, url, and title. | |
| 34 | 34 | */ |
| 35 | 35 | public class HyperlinkModel { |
| ... | ||
| 44 | 44 | * |
| 45 | 45 | * @param text The hyperlink text displayed (e.g., displayed to the user). |
| 46 | * @param url The destination URL (e.g., when clicked). | |
| 46 | * @param url The destination URL (e.g., when clicked). | |
| 47 | 47 | */ |
| 48 | 48 | public HyperlinkModel( final String text, final String url ) { |
| 49 | 49 | this( text, url, null ); |
| 50 | 50 | } |
| 51 | 51 | |
| 52 | 52 | /** |
| 53 | 53 | * Constructs a new hyperlink model for the given AST link. |
| 54 | * | |
| 54 | * | |
| 55 | 55 | * @param link A markdown link. |
| 56 | 56 | */ |
| 57 | 57 | public HyperlinkModel( final Link link ) { |
| 58 | 58 | this( |
| 59 | link.getText().toString(), | |
| 60 | link.getUrl().toString(), | |
| 61 | link.getTitle().toString() | |
| 59 | link.getText().toString(), | |
| 60 | link.getUrl().toString(), | |
| 61 | link.getTitle().toString() | |
| 62 | 62 | ); |
| 63 | 63 | } |
| 64 | 64 | |
| 65 | 65 | /** |
| 66 | 66 | * Constructs a new hyperlink model in Markdown format by default. |
| 67 | 67 | * |
| 68 | * @param text The hyperlink text displayed (e.g., displayed to the user). | |
| 69 | * @param url The destination URL (e.g., when clicked). | |
| 68 | * @param text The hyperlink text displayed (e.g., displayed to the user). | |
| 69 | * @param url The destination URL (e.g., when clicked). | |
| 70 | 70 | * @param title The hyperlink title (e.g., shown as a tooltip). |
| 71 | 71 | */ |
| 72 | public HyperlinkModel( final String text, final String url, final String title ) { | |
| 72 | public HyperlinkModel( final String text, final String url, | |
| 73 | final String title ) { | |
| 73 | 74 | setText( text ); |
| 74 | 75 | setUrl( url ); |
| 34 | 34 | |
| 35 | 35 | /** |
| 36 | * Visits hyperlinks in a document so that the user can edit the hyperlink | |
| 37 | * within a dialog. | |
| 36 | * Responsible for extracting a hyperlink from the document so that the user | |
| 37 | * can edit the link within a dialog. | |
| 38 | 38 | */ |
| 39 | 39 | public class LinkVisitor { |
| 58 | 58 | |
| 59 | 59 | /** |
| 60 | * Markdown editor pane. | |
| 60 | * Provides the ability to edit a text document. | |
| 61 | 61 | */ |
| 62 | 62 | public class MarkdownEditorPane extends EditorPane { |
| 49 | 49 | import static com.scrivenvar.Messages.get; |
| 50 | 50 | |
| 51 | /** | |
| 52 | * Responsible for user preferences that can be changed from the GUI. The | |
| 53 | * settings are displayed and persisted using {@link PreferencesFx}. | |
| 54 | */ | |
| 51 | 55 | public class UserPreferences { |
| 52 | 56 | private final Settings SETTINGS = Services.load( Settings.class ); |
| 54 | 54 | import java.nio.file.Path; |
| 55 | 55 | |
| 56 | import static com.scrivenvar.Constants.PARAGRAPH_ID_PREFIX; | |
| 57 | import static com.scrivenvar.Constants.STYLESHEET_PREVIEW; | |
| 56 | import static com.scrivenvar.Constants.*; | |
| 58 | 57 | import static java.awt.Desktop.Action.BROWSE; |
| 59 | 58 | import static java.awt.Desktop.getDesktop; |
| ... | ||
| 177 | 176 | private final CustomImageLoader mImageLoader = new CustomImageLoader(); |
| 178 | 177 | |
| 179 | private Path mPath; | |
| 178 | private Path mPath = DEFAULT_DIRECTORY; | |
| 180 | 179 | |
| 181 | 180 | /** |
| ... | ||
| 196 | 195 | NO_OP_REPAINT_LISTENER, mImageLoader ) ); |
| 197 | 196 | |
| 198 | // Ensure fonts are always anti-aliased. | |
| 199 | 197 | final var context = getSharedContext(); |
| 200 | 198 | context.setReplacedElementFactory( factory ); |
| 53 | 53 | import static org.apache.batik.util.XMLResourceDescriptor.getXMLParserClassName; |
| 54 | 54 | |
| 55 | /** | |
| 56 | * Responsible for converting SVG images into rasterized PNG images. | |
| 57 | */ | |
| 55 | 58 | public class SVGRasterizer { |
| 56 | 59 | private final static Notifier NOTIFIER = Services.load( Notifier.class ); |
| 46 | 46 | import static com.scrivenvar.preview.SVGRasterizer.rasterize; |
| 47 | 47 | |
| 48 | /** | |
| 49 | * Responsible for running {@link SVGRasterizer} on SVG images detected within | |
| 50 | * a document to transform them into rasterized versions. | |
| 51 | */ | |
| 48 | 52 | public class SVGReplacedElementFactory |
| 49 | 53 | implements ReplacedElementFactory { |
| 40 | 40 | import java.util.Map; |
| 41 | 41 | |
| 42 | import static com.scrivenvar.Constants.*; | |
| 42 | import static com.scrivenvar.Constants.STATUS_PARSE_ERROR; | |
| 43 | 43 | import static com.scrivenvar.Messages.get; |
| 44 | 44 | import static com.scrivenvar.decorators.RVariableDecorator.PREFIX; |
| ... | ||
| 76 | 76 | private static final ScriptEngine ENGINE = |
| 77 | 77 | (new ScriptEngineManager()).getEngineByName( "Renjin" ); |
| 78 | ||
| 79 | private static final int PREFIX_LENGTH = PREFIX.length(); | |
| 78 | 80 | |
| 79 | 81 | /** |
| ... | ||
| 122 | 124 | public String process( final String text ) { |
| 123 | 125 | final int length = text.length(); |
| 124 | final int prefixLength = PREFIX.length(); | |
| 125 | 126 | |
| 126 | 127 | // The * 2 is a wild guess at the ratio of R statements to the length |
| ... | ||
| 136 | 137 | |
| 137 | 138 | // Jump to the start of the R statement. |
| 138 | prevIndex = currIndex + prefixLength; | |
| 139 | prevIndex = currIndex + PREFIX_LENGTH; | |
| 139 | 140 | |
| 140 | 141 | // Find the statement ending (`), without indexing past the text boundary. |
| 46 | 46 | |
| 47 | 47 | import java.io.File; |
| 48 | import java.io.FileNotFoundException; | |
| 48 | 49 | import java.nio.file.Path; |
| 49 | 50 | |
| ... | ||
| 68 | 69 | * @return The new {@link ImageLinkExtension}, never {@code null}. |
| 69 | 70 | */ |
| 70 | public static ImageLinkExtension create( final Path path ) { | |
| 71 | public static ImageLinkExtension create( @NotNull final Path path ) { | |
| 71 | 72 | return new ImageLinkExtension( path ); |
| 72 | 73 | } |
| ... | ||
| 82 | 83 | private class ImageLinkResolver implements LinkResolver { |
| 83 | 84 | private final UserPreferences mUserPref = getUserPreferences(); |
| 84 | private final String mImagePrefix = | |
| 85 | mUserPref.getImagesDirectory().toString(); | |
| 86 | private final String mImageSuffixes = mUserPref.getImagesOrder(); | |
| 85 | private final File mImagesUserPrefix = mUserPref.getImagesDirectory(); | |
| 86 | private final String mImageExtensions = mUserPref.getImagesOrder(); | |
| 87 | 87 | |
| 88 | 88 | public ImageLinkResolver() { |
| 89 | 89 | } |
| 90 | 90 | |
| 91 | // you can also set/clear/modify attributes through | |
| 92 | // ResolvedLink.getAttributes() and | |
| 93 | // ResolvedLink.getNonNullAttributes() | |
| 91 | /** | |
| 92 | * You can also set/clear/modify attributes through | |
| 93 | * {@link ResolvedLink#getAttributes()} and | |
| 94 | * {@link ResolvedLink#getNonNullAttributes()}. | |
| 95 | */ | |
| 94 | 96 | @NotNull |
| 95 | 97 | @Override |
| 96 | 98 | public ResolvedLink resolveLink( |
| 97 | 99 | @NotNull final Node node, |
| 98 | 100 | @NotNull final LinkResolverBasicContext context, |
| 99 | 101 | @NotNull final ResolvedLink link ) { |
| 100 | 102 | return node instanceof Image ? resolve( link ) : link; |
| 101 | 103 | } |
| 102 | 104 | |
| 103 | @NotNull | |
| 104 | private ResolvedLink resolve( @NotNull final ResolvedLink link ) { | |
| 105 | private ResolvedLink resolve( final ResolvedLink link ) { | |
| 105 | 106 | String url = link.getUrl(); |
| 106 | 107 | |
| 107 | 108 | try { |
| 108 | final String imageFile = format( "%s/%s", getImagePrefix(), url ); | |
| 109 | final String suffixes = getImageSuffixes(); | |
| 110 | final String editDir = getEditDirectory(); | |
| 109 | final Path imagePrefix = getImagePrefix().toPath(); | |
| 110 | ||
| 111 | // Path to the file being edited. | |
| 112 | Path editPath = getEditPath(); | |
| 113 | ||
| 114 | // If there is no parent path to the file, it means the file has not | |
| 115 | // been saved. Default to using the value from the user's preferences. | |
| 116 | // The user's preferences will be defaulted to a the application's | |
| 117 | // starting directory. | |
| 118 | if( editPath == null ) { | |
| 119 | editPath = imagePrefix; | |
| 120 | } | |
| 121 | else { | |
| 122 | editPath = Path.of( editPath.toString(), imagePrefix.toString() ); | |
| 123 | } | |
| 124 | ||
| 125 | final Path imagePathPrefix = Path.of( editPath.toString(), url ); | |
| 126 | final String suffixes = getImageExtensions(); | |
| 127 | boolean missing = true; | |
| 111 | 128 | |
| 112 | 129 | for( final String ext : Splitter.on( ' ' ).split( suffixes ) ) { |
| 113 | final String imagePath = format( | |
| 114 | "%s/%s.%s", editDir, imageFile, ext ); | |
| 130 | final String imagePath = format( "%s.%s", imagePathPrefix, ext ); | |
| 115 | 131 | final File file = new File( imagePath ); |
| 116 | 132 | |
| 117 | 133 | if( file.exists() ) { |
| 118 | 134 | url = file.toString(); |
| 135 | missing = false; | |
| 119 | 136 | break; |
| 120 | 137 | } |
| 138 | } | |
| 139 | ||
| 140 | if( missing ) { | |
| 141 | throw new FileNotFoundException( imagePathPrefix + ".*" ); | |
| 121 | 142 | } |
| 122 | 143 | |
| 123 | 144 | final String protocol = ProtocolResolver.getProtocol( url ); |
| 124 | 145 | if( "file".equals( protocol ) ) { |
| 125 | 146 | url = "file://" + url; |
| 126 | 147 | } |
| 148 | ||
| 149 | getNotifier().clear(); | |
| 127 | 150 | |
| 128 | 151 | return link.withStatus( LinkStatus.VALID ).withUrl( url ); |
| 129 | 152 | } catch( final Exception e ) { |
| 130 | getNotifier().notify( e ); | |
| 153 | getNotifier().notify( "File not found: " + e.getLocalizedMessage() ); | |
| 131 | 154 | } |
| 132 | 155 | |
| 133 | 156 | return link; |
| 134 | 157 | } |
| 135 | 158 | |
| 136 | private String getImagePrefix() { | |
| 137 | return mImagePrefix; | |
| 159 | private File getImagePrefix() { | |
| 160 | return mImagesUserPrefix; | |
| 138 | 161 | } |
| 139 | 162 | |
| 140 | private String getImageSuffixes() { | |
| 141 | return mImageSuffixes; | |
| 163 | private String getImageExtensions() { | |
| 164 | return mImageExtensions; | |
| 142 | 165 | } |
| 143 | 166 | |
| 144 | private String getEditDirectory() { | |
| 145 | return mPath.getParent().toString(); | |
| 167 | private Path getEditPath() { | |
| 168 | return mPath.getParent(); | |
| 146 | 169 | } |
| 147 | 170 | } |
| 148 | 171 | |
| 149 | 172 | private final Path mPath; |
| 150 | 173 | |
| 151 | private ImageLinkExtension( final Path path ) { | |
| 174 | private ImageLinkExtension( @NotNull final Path path ) { | |
| 152 | 175 | mPath = path; |
| 153 | 176 | } |
| 85 | 85 | */ |
| 86 | 86 | List<String> getStringSettingList( String property ); |
| 87 | ||
| 88 | /** | |
| 89 | * Changes key's value. This will clear the old value before setting the | |
| 90 | * new value so that the old value is erased, not changed into a list. | |
| 91 | * | |
| 92 | * @param key The property key name to obtain its value. | |
| 93 | * @param value The new value to set. | |
| 94 | */ | |
| 95 | void putSetting( String key, String value ); | |
| 96 | 87 | } |
| 97 | 88 |
| 82 | 82 | |
| 83 | 83 | /** |
| 84 | * Changes key's value. This will clear the old value before setting the new | |
| 85 | * value so that the old value is erased, not changed into a list. | |
| 86 | * | |
| 87 | * @param key The property key name to obtain its value. | |
| 88 | * @param value The new value to set. | |
| 89 | */ | |
| 90 | @Override | |
| 91 | public void putSetting( final String key, final String value ) { | |
| 92 | getSettings().clearProperty( key ); | |
| 93 | getSettings().addProperty( key, value ); | |
| 94 | } | |
| 95 | ||
| 96 | /** | |
| 97 | 84 | * Convert the generic list of property objects into strings. |
| 98 | 85 | * |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.spelling.api; | |
| 29 | ||
| 30 | import java.util.List; | |
| 31 | import java.util.function.BiConsumer; | |
| 32 | ||
| 33 | /** | |
| 34 | * Defines the responsibilities for a spell checking API. The intention is | |
| 35 | * to allow different spell checking implementations to be used by the | |
| 36 | * application, such as SymSpell and LinSpell. | |
| 37 | */ | |
| 38 | public interface SpellChecker { | |
| 39 | ||
| 40 | /** | |
| 41 | * Answers whether the given lexeme, in whole, is found in the lexicon. The | |
| 42 | * lexicon lookup is performed case-insensitively. This method should be | |
| 43 | * used instead of {@link #suggestions(String, int)} for performance reasons. | |
| 44 | * | |
| 45 | * @param lexeme The word to check for correctness. | |
| 46 | * @return {@code true} if the lexeme is in the lexicon. | |
| 47 | */ | |
| 48 | boolean inLexicon( String lexeme ); | |
| 49 | ||
| 50 | /** | |
| 51 | * Gets a list of spelling corrections for the given lexeme. | |
| 52 | * | |
| 53 | * @param lexeme A word to check for correctness that's not in the lexicon. | |
| 54 | * @param count The maximum number of alternatives to return. | |
| 55 | * @return A list of words in the lexicon that are similar to the given | |
| 56 | * lexeme. | |
| 57 | */ | |
| 58 | List<String> suggestions( String lexeme, int count ); | |
| 59 | ||
| 60 | /** | |
| 61 | * Iterates over the given text, emitting starting and ending offsets into | |
| 62 | * the text for every word that is missing from the lexicon. | |
| 63 | * | |
| 64 | * @param text The text to check for words missing from the lexicon. | |
| 65 | * @param consumer Every missing word emits a message with the starting | |
| 66 | * and ending offset into the text where said word is found. | |
| 67 | */ | |
| 68 | void proofread( String text, BiConsumer<Integer, Integer> consumer ); | |
| 69 | } | |
| 1 | 70 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.spelling.impl; | |
| 29 | ||
| 30 | import com.scrivenvar.spelling.api.SpellChecker; | |
| 31 | ||
| 32 | import java.util.List; | |
| 33 | import java.util.function.BiConsumer; | |
| 34 | ||
| 35 | /** | |
| 36 | * Responsible for spell checking in the event that a real spell checking | |
| 37 | * implementation cannot be created (for any reason). Does not perform any | |
| 38 | * spell checking and indicates that any given lexeme is in the lexicon. | |
| 39 | */ | |
| 40 | public class PermissiveSpeller implements SpellChecker { | |
| 41 | /** | |
| 42 | * Returns {@code true}, ignoring the given word. | |
| 43 | * | |
| 44 | * @param ignored Unused. | |
| 45 | * @return {@code true} | |
| 46 | */ | |
| 47 | @Override | |
| 48 | public boolean inLexicon( final String ignored ) { | |
| 49 | return true; | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * Returns an array with the given lexeme. | |
| 54 | * | |
| 55 | * @param lexeme The word to return. | |
| 56 | * @param ignored Unused. | |
| 57 | * @return A suggestion list containing the given lexeme. | |
| 58 | */ | |
| 59 | @Override | |
| 60 | public List<String> suggestions( final String lexeme, final int ignored ) { | |
| 61 | return List.of( lexeme ); | |
| 62 | } | |
| 63 | ||
| 64 | /** | |
| 65 | * Performs no action. | |
| 66 | * | |
| 67 | * @param text Unused. | |
| 68 | * @param ignored Uncalled. | |
| 69 | */ | |
| 70 | @Override | |
| 71 | public void proofread( | |
| 72 | final String text, final BiConsumer<Integer, Integer> ignored ) { | |
| 73 | } | |
| 74 | } | |
| 1 | 75 |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.spelling.impl; | |
| 29 | ||
| 30 | import com.scrivenvar.spelling.api.SpellChecker; | |
| 31 | import io.gitlab.rxp90.jsymspell.SuggestItem; | |
| 32 | import io.gitlab.rxp90.jsymspell.SymSpell; | |
| 33 | import io.gitlab.rxp90.jsymspell.SymSpellBuilder; | |
| 34 | ||
| 35 | import java.text.BreakIterator; | |
| 36 | import java.util.ArrayList; | |
| 37 | import java.util.Collection; | |
| 38 | import java.util.List; | |
| 39 | import java.util.function.BiConsumer; | |
| 40 | ||
| 41 | import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity; | |
| 42 | import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.ALL; | |
| 43 | import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.CLOSEST; | |
| 44 | import static java.lang.Character.isLetter; | |
| 45 | ||
| 46 | /** | |
| 47 | * Responsible for spell checking using {@link SymSpell}. | |
| 48 | */ | |
| 49 | public class SymSpellSpeller implements SpellChecker { | |
| 50 | private final SymSpell mSymSpell; | |
| 51 | ||
| 52 | /** | |
| 53 | * Creates a new lexicon for the given collection of lexemes. | |
| 54 | * | |
| 55 | * @param lexiconWords The words in the lexicon to add for spell checking, | |
| 56 | * must not be empty. | |
| 57 | * @return An instance of {@link SpellChecker} that can check if a word | |
| 58 | * is correct and suggest alternatives. | |
| 59 | */ | |
| 60 | public static SpellChecker forLexicon( | |
| 61 | final Collection<String> lexiconWords ) { | |
| 62 | assert lexiconWords != null && !lexiconWords.isEmpty(); | |
| 63 | ||
| 64 | final SymSpellBuilder builder = new SymSpellBuilder() | |
| 65 | .setLexiconWords( lexiconWords ); | |
| 66 | ||
| 67 | return new SymSpellSpeller( builder.build() ); | |
| 68 | } | |
| 69 | ||
| 70 | /** | |
| 71 | * Prevent direct instantiation so that only the {@link SpellChecker} | |
| 72 | * interface | |
| 73 | * is available. | |
| 74 | * | |
| 75 | * @param symSpell The implementation-specific spell checker. | |
| 76 | */ | |
| 77 | private SymSpellSpeller( final SymSpell symSpell ) { | |
| 78 | mSymSpell = symSpell; | |
| 79 | } | |
| 80 | ||
| 81 | @Override | |
| 82 | public boolean inLexicon( final String lexeme ) { | |
| 83 | return lookup( lexeme, CLOSEST ).size() == 1; | |
| 84 | } | |
| 85 | ||
| 86 | @Override | |
| 87 | public List<String> suggestions( final String lexeme, int count ) { | |
| 88 | final List<String> result = new ArrayList<>( count ); | |
| 89 | ||
| 90 | for( final var item : lookup( lexeme, ALL ) ) { | |
| 91 | if( count-- > 0 ) { | |
| 92 | result.add( item.getSuggestion() ); | |
| 93 | } | |
| 94 | else { | |
| 95 | break; | |
| 96 | } | |
| 97 | } | |
| 98 | ||
| 99 | return result; | |
| 100 | } | |
| 101 | ||
| 102 | @Override | |
| 103 | public void proofread( | |
| 104 | final String text, final BiConsumer<Integer, Integer> consumer ) { | |
| 105 | assert text != null; | |
| 106 | assert consumer != null; | |
| 107 | ||
| 108 | final BreakIterator wb = BreakIterator.getWordInstance(); | |
| 109 | wb.setText( text ); | |
| 110 | ||
| 111 | int boundaryIndex = wb.first(); | |
| 112 | int previousIndex = 0; | |
| 113 | ||
| 114 | while( boundaryIndex != BreakIterator.DONE ) { | |
| 115 | final String substring = | |
| 116 | text.substring( previousIndex, boundaryIndex ).toLowerCase(); | |
| 117 | ||
| 118 | if( isWord( substring ) && !inLexicon( substring ) ) { | |
| 119 | consumer.accept( previousIndex, boundaryIndex ); | |
| 120 | } | |
| 121 | ||
| 122 | previousIndex = boundaryIndex; | |
| 123 | boundaryIndex = wb.next(); | |
| 124 | } | |
| 125 | } | |
| 126 | ||
| 127 | /** | |
| 128 | * Answers whether the given string is likely a word by checking the first | |
| 129 | * character. | |
| 130 | * | |
| 131 | * @param word The word to check. | |
| 132 | * @return {@code true} if the word begins with a letter. | |
| 133 | */ | |
| 134 | private boolean isWord( final String word ) { | |
| 135 | return !word.isEmpty() && isLetter( word.charAt( 0 ) ); | |
| 136 | } | |
| 137 | ||
| 138 | /** | |
| 139 | * Returns a list of {@link SuggestItem} instances that provide alternative | |
| 140 | * spellings for the given lexeme. | |
| 141 | * | |
| 142 | * @param lexeme A word to look up in the lexicon. | |
| 143 | * @param v Influences the number of results returned. | |
| 144 | * @return Alternative lexemes. | |
| 145 | */ | |
| 146 | private List<SuggestItem> lookup( final String lexeme, final Verbosity v ) { | |
| 147 | return getSpeller().lookup( lexeme, v ); | |
| 148 | } | |
| 149 | ||
| 150 | private SymSpell getSpeller() { | |
| 151 | return mSymSpell; | |
| 152 | } | |
| 153 | } | |
| 1 | 154 |
| 34 | 34 | |
| 35 | 35 | /** |
| 36 | * Simple action class | |
| 36 | * Defines actions the user can take by interacting with the GUI. | |
| 37 | 37 | */ |
| 38 | 38 | public class Action { |
| 1 | /* | |
| 2 | * Copyright 2020 White Magic Software, Ltd. | |
| 3 | * | |
| 4 | * All rights reserved. | |
| 5 | * | |
| 6 | * Redistribution and use in source and binary forms, with or without | |
| 7 | * modification, are permitted provided that the following conditions are met: | |
| 8 | * | |
| 9 | * o Redistributions of source code must retain the above copyright | |
| 10 | * notice, this list of conditions and the following disclaimer. | |
| 11 | * | |
| 12 | * o Redistributions in binary form must reproduce the above copyright | |
| 13 | * notice, this list of conditions and the following disclaimer in the | |
| 14 | * documentation and/or other materials provided with the distribution. | |
| 15 | * | |
| 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 20 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 27 | */ | |
| 28 | package com.scrivenvar.util; | |
| 29 | ||
| 30 | import java.util.List; | |
| 31 | ||
| 32 | /** | |
| 33 | * Convenience class that provides a clearer API for obtaining list elements. | |
| 34 | */ | |
| 35 | public final class Lists { | |
| 36 | ||
| 37 | private Lists() { | |
| 38 | } | |
| 39 | ||
| 40 | /** | |
| 41 | * Returns the first item in the given list, or null if not found. | |
| 42 | * | |
| 43 | * @param <T> The generic list type. | |
| 44 | * @param list The list that may have a first item. | |
| 45 | * | |
| 46 | * @return null if the list is null or there is no first item. | |
| 47 | */ | |
| 48 | public static <T> T getFirst( final List<T> list ) { | |
| 49 | return getFirst( list, null ); | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * Returns the last item in the given list, or null if not found. | |
| 54 | * | |
| 55 | * @param <T> The generic list type. | |
| 56 | * @param list The list that may have a last item. | |
| 57 | * | |
| 58 | * @return null if the list is null or there is no last item. | |
| 59 | */ | |
| 60 | public static <T> T getLast( final List<T> list ) { | |
| 61 | return getLast( list, null ); | |
| 62 | } | |
| 63 | ||
| 64 | /** | |
| 65 | * Returns the first item in the given list, or t if not found. | |
| 66 | * | |
| 67 | * @param <T> The generic list type. | |
| 68 | * @param list The list that may have a first item. | |
| 69 | * @param t The default return value. | |
| 70 | * | |
| 71 | * @return null if the list is null or there is no first item. | |
| 72 | */ | |
| 73 | public static <T> T getFirst( final List<T> list, final T t ) { | |
| 74 | return isEmpty( list ) ? t : list.get( 0 ); | |
| 75 | } | |
| 76 | ||
| 77 | /** | |
| 78 | * Returns the last item in the given list, or t if not found. | |
| 79 | * | |
| 80 | * @param <T> The generic list type. | |
| 81 | * @param list The list that may have a last item. | |
| 82 | * @param t The default return value. | |
| 83 | * | |
| 84 | * @return null if the list is null or there is no last item. | |
| 85 | */ | |
| 86 | public static <T> T getLast( final List<T> list, final T t ) { | |
| 87 | return isEmpty( list ) ? t : list.get( list.size() - 1 ); | |
| 88 | } | |
| 89 | ||
| 90 | /** | |
| 91 | * Returns true if the given list is null or empty. | |
| 92 | * | |
| 93 | * @param <T> The generic list type. | |
| 94 | * @param list The list that has a last item. | |
| 95 | * | |
| 96 | * @return true The list is empty. | |
| 97 | */ | |
| 98 | public static <T> boolean isEmpty( final List<T> list ) { | |
| 99 | return list == null || list.isEmpty(); | |
| 100 | } | |
| 101 | } | |
| 102 | 1 |
| 28 | 28 | |
| 29 | 29 | .markdown-editor { |
| 30 | -fx-font-size: 14px; | |
| 30 | -fx-font-size: 11pt; | |
| 31 | 31 | } |
| 32 | 32 | |
| ... | ||
| 39 | 39 | .markdown-editor .selection { |
| 40 | 40 | -fx-fill: #a6d2ff; |
| 41 | } | |
| 42 | ||
| 43 | /* Decoration for words not found in the lexicon. */ | |
| 44 | .markdown-editor .spelling { | |
| 45 | -rtfx-underline-color: rgba(255, 131, 67, .9); | |
| 46 | -rtfx-underline-dash-array: 4, 2; | |
| 47 | -rtfx-underline-width: 2; | |
| 41 | 48 | } |
| 42 | 49 | |
| 144 | 144 | |
| 145 | 145 | # ######################################################################## |
| 146 | # Browse Directory | |
| 147 | # ######################################################################## | |
| 148 | ||
| 149 | BrowseDirectoryButton.chooser.title=Browse for local folder | |
| 150 | BrowseDirectoryButton.tooltip=${BrowseDirectoryButton.chooser.title} | |
| 151 | ||
| 152 | # ######################################################################## | |
| 153 | 146 | # Browse File |
| 154 | 147 | # ######################################################################## |
| ... | ||
| 188 | 181 | Dialog.about.header=${Main.title} |
| 189 | 182 | Dialog.about.content=Copyright 2020 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber |
| 190 | ||
| 191 | # Options ################################################################ | |
| 192 | ||
| 193 | # ######################################################################## | |
| 194 | # Options Dialog | |
| 195 | # ######################################################################## | |
| 196 | ||
| 197 | OptionsDialog.title=Options | |
| 198 | OptionsDialog.generalTab.text=General | |
| 199 | OptionsDialog.markdownTab.text=Markdown | |
| 200 | ||
| 201 | # ######################################################################## | |
| 202 | # General Options Pane | |
| 203 | # ######################################################################## | |
| 204 | ||
| 205 | GeneralOptionsPane.encodingLabel.text=En_coding\: | |
| 206 | GeneralOptionsPane.lineSeparatorLabel.text=_Line separator\: | |
| 207 | GeneralOptionsPane.lineSeparatorLabel2.text=(applies to new files only) | |
| 208 | ||
| 209 | GeneralOptionsPane.platformDefault=Platform Default ({0}) | |
| 210 | GeneralOptionsPane.sepWindows=Windows (CRLF) | |
| 211 | GeneralOptionsPane.sepUnix=Unix (LF) | |
| 212 | ||
| 213 | # ######################################################################## | |
| 214 | # Markdown Options Pane | |
| 215 | # ######################################################################## | |
| 216 | ||
| 217 | MarkdownOptionsPane.abbreviationsExtCheckBox.text=A_bbreviations in the way of | |
| 218 | MarkdownOptionsPane.abbreviationsExtLink.text=Markdown Extra | |
| 219 | MarkdownOptionsPane.anchorlinksExtCheckBox.text=_Anchor links in headers | |
| 220 | MarkdownOptionsPane.atxHeaderSpaceExtCheckBox.text=Requires a space char after Atx \# header prefixes, so that \#dasdsdaf is not a header | |
| 221 | MarkdownOptionsPane.autolinksExtCheckBox.text=_Plain (undelimited) autolinks in the way of | |
| 222 | MarkdownOptionsPane.autolinksExtLink.text=Github-flavoured-Markdown | |
| 223 | MarkdownOptionsPane.definitionListsExtCheckBox.text=_Definition lists in the way of | |
| 224 | MarkdownOptionsPane.definitionListsExtLink.text=Markdown Extra | |
| 225 | MarkdownOptionsPane.extAnchorLinksExtCheckBox.text=Generate anchor links for headers using complete contents of the header | |
| 226 | MarkdownOptionsPane.fencedCodeBlocksExtCheckBox.text=_Fenced Code Blocks in the way of | |
| 227 | MarkdownOptionsPane.fencedCodeBlocksExtLabel.text=or | |
| 228 | MarkdownOptionsPane.fencedCodeBlocksExtLink.text=Markdown Extra | |
| 229 | MarkdownOptionsPane.fencedCodeBlocksExtLink2.text=Github-flavoured-Markdown | |
| 230 | MarkdownOptionsPane.forceListItemParaExtCheckBox.text=Force List and Definition Paragraph wrapping if it includes more than just a single paragraph | |
| 231 | MarkdownOptionsPane.hardwrapsExtCheckBox.text=_Newlines in paragraph-like content as real line breaks, see | |
| 232 | MarkdownOptionsPane.hardwrapsExtLink.text=Github-flavoured-Markdown | |
| 233 | MarkdownOptionsPane.quotesExtCheckBox.text=Beautify single _quotes, double quotes and double angle quotes (\u00ab and \u00bb) | |
| 234 | MarkdownOptionsPane.relaxedHrRulesExtCheckBox.text=Allow horizontal rules without a blank line following them | |
| 235 | MarkdownOptionsPane.smartsExtCheckBox.text=Beautify apostrophes, _ellipses ("..." and ". . .") and dashes ("--" and "---") | |
| 236 | MarkdownOptionsPane.strikethroughExtCheckBox.text=_Strikethrough | |
| 237 | MarkdownOptionsPane.suppressHtmlBlocksExtCheckBox.text=Suppress the _output of HTML blocks | |
| 238 | MarkdownOptionsPane.suppressInlineHtmlExtCheckBox.text=Suppress the o_utput of inline HTML elements | |
| 239 | MarkdownOptionsPane.tablesExtCheckBox.text=_Tables similar to | |
| 240 | MarkdownOptionsPane.tablesExtLabel.text=(like | |
| 241 | MarkdownOptionsPane.tablesExtLabel2.text=tables, but with colspan support) | |
| 242 | MarkdownOptionsPane.tablesExtLink.text=MultiMarkdown | |
| 243 | MarkdownOptionsPane.tablesExtLink2.text=Markdown Extra | |
| 244 | MarkdownOptionsPane.taskListItemsExtCheckBox.text=GitHub style task list items | |
| 245 | MarkdownOptionsPane.wikilinksExtCheckBox.text=_Wiki-style links ("[[wiki link]]") | |
| 246 | 183 | |
| 1 | # Building | |
| 2 | ||
| 3 | The lexicon files are retrieved from SymSpell in the parent directory: | |
| 4 | ||
| 5 | svn export \ | |
| 6 | https://github.com/wolfgarbe/SymSpell/trunk/SymSpell.FrequencyDictionary/ lexicons | |
| 7 | ||
| 8 | The lexicons and bigrams are both space-separated, but parsing a | |
| 9 | tab-delimited file is easier, so change them to tab-separated files. | |
| 1 | 10 |