diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..11aafdf --- /dev/null +++ b/.gitattributes @@ -0,0 +1,32 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Java sources +*.java text diff=java eol=lf +*.gradle text diff=java eol=lf + +# These files are text and should be normalized (Convert crlf => lf) +*.css text diff=css eol=lf +*.html text diff=html eol=lf +*.md text diff=markdown eol=lf +*.js text eol=lf +*.csv text eol=lf +*.json text eol=lf +*.properties text eol=lf +*.svg text eol=lf +*.xml text eol=lf +*.yaml text eol=lf +*.yml text eol=lf + +# These files are binary and should be left untouched +*.png binary +*.gif binary +*.jpg binary +*.jpeg binary + +# Common build-tool wrapper scripts +mvnw text eol=lf +gradlew text eol=lf +*.sh text eol=lf +*.bat text eol=crlf +*.cmd text eol=crlf \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8b170e9..ca4ebb8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ .metadata/** .recommenders/** .settings/** +.gradle/** bin/** target/** +build/** .classpath .project diff --git a/LICENSE b/LICENSE index 6e5fc0b..ecbc059 100644 --- a/LICENSE +++ b/LICENSE @@ -1,9 +1,339 @@ -The MIT License (MIT) + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 -Copyright (c) 2018 Matthew Coley + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + Preamble -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/README.md b/README.md index 164d15b..78e1f92 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,24 @@ # Code2HTML -A JavaFX app for converting user specified languages to HTML. Paste source code into the top-left panel and the HTML will show in the top-right, with a preview at the bottom. -The CSS and _optional JS_ are available in the other tabs in the top-right. Updating the CSS and JS will display live updates in the preview area. +A JavaFX app for converting user specified languages to HTML. -### Top-Left tabs: +**Requirements** -* **HTML**: The HTML output. -* **CSS**: The CSS code that styles the HTML span tags. -* **JS**: Optional JS for manual inclusion of collapse-sections. -* **Patterns**: List of Regex groups for the currently loaded language. - -While the JS is not necessary it allows you to make portions of the code collapseable. - -## Download - -See the [releases](https://github.com/Col-E/Code2HTML/releases) page for the latest build. Or compile with maven via `mvn package` - -**Note**: Builds are based off of Java 8. Running on later versions will not work, please see [the update guide](UPDATING-JDK.md) for more information. +- Versions `4.X.X` and beyond require Java 17 to run _(JavaFX bundled)_ +- Versions `3.X.X` and below require Java 8 to run _(JavaFX not bundled)_ ## Screenshots * ![Main View](ss-html.png) * ![Config View](ss-config.png) +## Download + +See the [releases](https://github.com/Col-E/Code2HTML/releases) page for the latest build. Or compile with maven via `mvn package` + ## Libraries used: -* [Apache Commons IO](https://commons.apache.org/proper/commons-io/) -* [Apache Commons Text](https://commons.apache.org/proper/commons-text/) -* [ControlsFX](https://github.com/controlsfx/controlsfx) -* [JRegex](http://jregex.sourceforge.net/) -* [picocli](https://picocli.info/) -* [Lombok](https://projectlombok.org/) \ No newline at end of file +* [Apache Commons Text](https://commons.apache.org/proper/commons-text/) - HTML escaping +* [Florian Ingerl's Regex](https://github.com/florianingerl/com.florianingerl.util.regex) - Drop in `java.util.regex` replacement that allows for `(?N)` recursion _(Also GPLv2)_ +* [JavaFX](https://openjfx.io/) - UI +* [picocli](https://picocli.info/) - CLI \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..9f5e1ce --- /dev/null +++ b/build.gradle @@ -0,0 +1,77 @@ +plugins { + id 'java' + id 'application' + id 'com.github.johnrengelman.shadow' version '8.1.1' + id 'org.openjfx.javafxplugin' version '0.1.0' +} + +group 'software.coley' +version '4.0.0-SNAPSHOT' + +repositories { + mavenLocal() + mavenCentral() + maven { url 'https://jitpack.io' } +} + +def javaFxVersion = '22-ea+28' +def javaFxIncludeInDist = System.getProperty('skip.jfx.bundle') == null + +javafx { + version = javaFxVersion + modules = ['javafx.controls', 'javafx.web'] +} + +dependencies { + implementation 'org.apache.commons:commons-text:1.11.0' + implementation 'com.github.florianingerl.util:regex:1.1.9' + implementation 'javax.xml.bind:jaxb-api:2.3.1' + implementation 'javax.activation:activation:1.1.1' + implementation 'com.sun.xml.bind:jaxb-impl:2.3.4' + implementation 'com.github.Col-E:tiwulfx-dock:1.2.3' + implementation 'info.picocli:picocli:4.7.5' + implementation 'fr.brouillard.oss:cssfx:11.5.1' + implementation 'org.openjfx:javafx-web:21' + implementation 'ch.qos.logback:logback-classic:1.4.14' + + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2' +} + +// https://docs.gradle.org/current/userguide/toolchains.html +// gradlew -q javaToolchains - see the list of detected toolchains. +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +// Append options for unchecked/deprecation +gradle.projectsEvaluated { + tasks.withType(JavaCompile).configureEach { + options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" + options.encoding = 'UTF-8' + options.incremental = true + } +} + +// All modules should have the same test framework setup. +test { + useJUnitPlatform() + + systemProperty 'junit.jupiter.execution.parallel.enabled', true + systemProperty 'junit.jupiter.execution.parallel.mode.default', 'concurrent' + + testLogging { + events "passed", "skipped", "failed" + } +} + +application { + mainClass = 'me.coley.c2h.Code2Html' +} + +shadowJar { + dependencies { + exclude(dependency(javaFxIncludeInDist ? 'invalid:invalid:invalid' : 'org.openjfx:.*:.*')) + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..5f1ed7b --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.caching=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7f93135 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3fa8f86 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 3a3a7f7..0000000 --- a/pom.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - 4.0.0 - me.coley - code2html - 3.5.0 - - - - org.apache.commons - commons-text - 1.6 - - - - commons-io - commons-io - 2.7 - - - - net.sourceforge.jregex - jregex - 1.2_01 - - - - org.controlsfx - controlsfx - 8.40.14 - - - - info.picocli - picocli - 3.9.5 - - - - org.projectlombok - lombok - 1.18.6 - provided - - - - junit - junit - 4.13.1 - test - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 8 - 8 - - - - maven-assembly-plugin - - - package - - single - - - - - - - me.coley.c2h.Code2Html - - - - jar-with-dependencies - - - - - - \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..f50a528 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,6 @@ +buildCache { + local { + enabled = true + directory = new File('build/cache') + } +} \ No newline at end of file diff --git a/src/main/java/me/coley/c2h/Code2Html.java b/src/main/java/me/coley/c2h/Code2Html.java index e3f90c8..f7c61c9 100644 --- a/src/main/java/me/coley/c2h/Code2Html.java +++ b/src/main/java/me/coley/c2h/Code2Html.java @@ -1,743 +1,25 @@ package me.coley.c2h; import javafx.application.Application; -import javafx.application.Platform; -import javafx.collections.ListChangeListener; -import javafx.geometry.Insets; -import javafx.geometry.Orientation; -import javafx.scene.Scene; -import javafx.scene.control.*; -import javafx.scene.input.*; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.GridPane; -import javafx.scene.text.Font; -import javafx.scene.web.WebView; -import javafx.stage.FileChooser; -import javafx.stage.Stage; -import javafx.util.Pair; -import jregex.Pattern; -import me.coley.c2h.config.*; -import me.coley.c2h.config.model.*; -import me.coley.c2h.ui.RuleCell; -import me.coley.c2h.util.Regex; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; -import org.controlsfx.validation.ValidationSupport; -import picocli.CommandLine; -import picocli.CommandLine.Option; - -import javax.xml.bind.JAXBException; -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.*; -import java.util.concurrent.Callable; -import java.util.stream.Collectors; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.controlsfx.validation.Validator.createEmptyValidator; -import static org.controlsfx.validation.Validator.createPredicateValidator; +import me.coley.c2h.ui.RootUI; /** - * Text to styled HTML powered by Regex. + * Main, pointing to UI {@link RootUI}. * - * @author Matt + * @author Matt Coley */ -public class Code2Html extends Application implements Callable { - // Base values - public static String BASE_CSS = ""; - public static String BASE_JS = ""; - // Command line options/args - @Option(names = {"-c", "--config"}, description = "Config to with languages and themes") - private File clConfig; - @Option(names = {"-l", "--language"}, description = "Language in config to use for parsing") - private String clLanguage; - @Option(names = {"-t", "--theme"}, description = "Theme in config to use for styling") - private String clTheme; - @Option(names = {"-v", "--clipboard"}, description = "Copy output to clipboard") - private boolean clClipboard; - @Option(names = {"-o", "--out"}, description = "The file to output converted HTML to") - private File clOutput; - @Option(names = {"-i", "--inline"}, description = "Option to make CSS inline with the HTML elements") - private boolean clInline; - @CommandLine.Parameters(index = "0", description = "The file to convert to styled HTML") - private File clInput; - // Other options - private boolean inline; - // Boolean indicating if CLI has priority over GUI - private boolean cliExecution; - // Controls - private WebView browser; - private TextArea txtInput; - private TextArea txtHTML; - private TextArea txtCSS; - private TextArea txtJS; - // Misc layout & stuff - private Menu mnLang; - private Menu mnTheme; - private BorderPane patternsPane; - private Stage stage; - private boolean previewLock; - // Config - private ConfigHelper helper; - +public class Code2Html { public static void main(String[] args) { - try { - BASE_CSS = IOUtils.toString(Code2Html.class.getResourceAsStream("/code.css"), UTF_8); - BASE_JS = IOUtils.toString(Code2Html.class.getResourceAsStream("/code.js"), UTF_8); - } catch(Exception e) { - e.printStackTrace(); - System.err.println("Failed to load default resources: css/js"); - System.exit(-1); - } - // Check command line values - boolean executed = invokeCmd(args); - // Invoke GUI if command line doesn't terminate - if (!executed) { - launch(args); - } - } - - private static boolean invokeCmd(String[] args) { - // Disable System.err so that GUI invokes don't print command-line usage - PrintStream ps = System.err; - System.setErr(new PrintStream(new ByteArrayOutputStream())); - // Call CLI - // If the input argument is missing, it won't even be invoked. - Code2Html cli = new Code2Html(); - CommandLine.call(cli, ps, args); - // Reset err - System.setErr(ps); - return cli.cliExecution; - } - - @Override - public Void call() { - try { - // Skip if no output flags - if(!clClipboard && clOutput == null) { - return null; - } else { - // Mark that CLI will be used instead of GUI - cliExecution = true; - } - // Verification - if (clOutput == null && !clClipboard) { - System.out.println("No output for converted content"); - System.exit(0); - } - if (!clInput.exists()) { - System.out.println("Input file does not exist"); - System.exit(0); - } - // Setup - Configuration configuration = clConfig == null ? Importer.importDefault() : Importer.importFromFile(clConfig.getAbsolutePath()); - Language language = clLanguage == null ? configuration.getLanguages().get(0) : configuration.findLanguage(clLanguage); - Theme theme = clTheme == null ? language.getThemes().get(0) : language.findTheme(clTheme); - helper = new ConfigHelper(configuration, language, theme); - // Input reading & parsing - String text = FileUtils.readFileToString(clInput, UTF_8); - String converted = helper.convert(text, clInline); - // Output - Platform.runLater(() -> { - if(clClipboard) { - Clipboard clip = Clipboard.getSystemClipboard(); - clip.clear(); - Map content = new HashMap<>(); - content.put(DataFormat.PLAIN_TEXT, converted); - clip.setContent(content); - } - if(clOutput != null) { - try { - FileUtils.write(clOutput, converted, UTF_8); - } catch(IOException e) { - System.out.println("Failed to write to file: " + clOutput); - System.out.println(e.getMessage()); - } - } - Platform.exit(); - }); - } catch(JAXBException e) { - System.out.println("Failed to parse config: " + clConfig); - System.out.println(e.getMessage()); - } catch(IOException e) { - System.out.println("Failed to read from config: " + clConfig); - System.out.println(e.getMessage()); - } - return null; + // TODO: Re-implement CLI + // - Options + // - language (default first lang in config) + // - outClipboard + // - outFile + // - inline css + // - Arg + // - 0: file + // - No args indicating CLI intended, then open GUI + + Application.launch(RootUI.class, args); } - - @Override - public void start(Stage stage) { - this.stage = stage; - try { - // Setup - Configuration configuration = Importer.importDefault(); - Language language = configuration.getLanguages().get(0); - Theme theme = language.getThemes().get(0); - helper = new ConfigHelper(configuration, language, theme); - } catch(Exception e) { - e.printStackTrace(); - fatal("Invalid default configuration", - "The default configuration file failed to be read.", - "Please ensure the default configuration file is properly formatted."); - } - browser = new WebView(); - txtInput = new TextArea(); - txtHTML = new TextArea(); - txtCSS = new TextArea(); - txtJS = new TextArea(); - mnLang = new Menu("Language"); - mnTheme = new Menu("Theme"); - patternsPane = new BorderPane(); - // Inputs - txtInput.setText("class Example { \n\t// put source code here\n}"); - txtInput.setFont(Font.font("monospace")); - txtHTML.setFont(Font.font("monospace")); - txtCSS.setFont(Font.font("monospace")); - txtJS.setFont(Font.font("monospace")); - txtHTML.setEditable(false); - updateCSS(); - txtJS.setText(BASE_JS); - txtInput.textProperty().addListener((ob, o, n) -> { - // Update HTML - updateHTML(); - }); - txtCSS.textProperty().addListener((ob, o, n) -> { - // Update style map - // - Assume 'pre' tag still exists. Hacky but should be fine - int index = txtCSS.getText().indexOf("pre {"); - String old = o.substring(0, index); - String cur = n.substring(0, index); - // Only update if the section for custom elements is updated. - if(!old.equalsIgnoreCase(cur)) { - // Iterate over css tags (by matching via regex) - for(Rule rule : helper.getRules()) { - Regex.getCssProperties(cur, "." + rule.getName()).forEach((key, value) -> { - helper.getTheme().update(rule.getName(), key, value); - }); - } - } - // Update HTML - updateHTML(); - }); - txtJS.textProperty().addListener((ob, o, n) -> { - // Update HTML - updateHTML(); - }); - // Config - updateRulesPane(); - // Tabs - TabPane tabs = new TabPane(); - Tab tabHTML = new Tab("HTML", txtHTML); - Tab tabCSS = new Tab("CSS", txtCSS); - Tab tabJS = new Tab("JS", txtJS); - Tab tabPatterns = new Tab("Patterns", patternsPane); - tabHTML.setClosable(false); - tabCSS.setClosable(false); - tabJS.setClosable(false); - tabPatterns.setClosable(false); - tabHTML.getStyleClass().add("tab-small"); - tabCSS.getStyleClass().add("tab-small"); - tabJS.getStyleClass().add("tab-small"); - tabPatterns.getStyleClass().add("tab-large"); - tabs.getTabs().add(tabHTML); - tabs.getTabs().add(tabCSS); - tabs.getTabs().add(tabJS); - tabs.getTabs().add(tabPatterns); - // Menubar - Menu mnFile = new Menu("File"); - MenuItem miConfigLoad = new MenuItem("Load config..."); - MenuItem miConfigSave = new MenuItem("Save config..."); - FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("Configurations", - "*.c2h", "*.xml"); - FileChooser fcSave = new FileChooser(); - fcSave.setInitialDirectory(new File(System.getProperty("user.dir"))); - fcSave.setTitle("Save configuration"); - fcSave.getExtensionFilters().add(extFilter); - FileChooser fcLoad = new FileChooser(); - fcLoad.setInitialDirectory(new File(System.getProperty("user.dir"))); - fcLoad.setTitle("Select configuration"); - fcLoad.getExtensionFilters().add(extFilter); - miConfigLoad.setOnAction(e -> { - File file = fcLoad.showOpenDialog(stage); - if(file != null) { - try { - Configuration configuration = Importer.importFromFile(file.getAbsolutePath()); - setConfiguration(configuration); - } catch(IOException ex) { - ex.printStackTrace(); - error("Configuration write failure", - "The configuration could not be read from the selected file.", - "Ensure the file location is valid (permissions, disk space)"); - } catch(JAXBException ex) { - ex.printStackTrace(); - error("Configuration load failure", - "The configuration could not be parsed.", - "The configuration instance could not be read from XML"); - - } - } - }); - miConfigSave.setOnAction(e -> { - File file = fcSave.showSaveDialog(stage); - if(file != null) { - try { - String text = Exporter.toString(helper.getConfiguration()); - Files.write(Paths.get(file.toURI()), text.getBytes(UTF_8)); - } catch(IOException ex) { - ex.printStackTrace(); - error("Configuration write failure", - "The configuration could not be written to the selected file.", - "Ensure the file location is valid (permissions, disk space)"); - } catch(JAXBException ex) { - ex.printStackTrace(); - error("Configuration write failure", - "The configuration could not be exported.", - "The configuration instance could not be translated into XML"); - } - } - }); - mnFile.getItems().addAll(miConfigLoad); - mnFile.getItems().addAll(miConfigSave); - // TODO: Swap out menu-item for checkbox - Menu mnOptions = new Menu("Options"); - CheckMenuItem miOptionInline = new CheckMenuItem("Inline HTML style"); - miOptionInline.selectedProperty().addListener((observable, oldValue, newValue) -> { - this.inline = newValue.booleanValue(); - updateHTML(); - }); - mnOptions.getItems().addAll(miOptionInline); - // inline - MenuBar menuBar = new MenuBar(); - menuBar.getMenus().addAll(mnFile, mnLang, mnTheme, mnOptions); - // Layout - SplitPane pane = new SplitPane(txtInput, tabs); - SplitPane vert = new SplitPane(pane, browser); - vert.setOrientation(Orientation.VERTICAL); - BorderPane wrap = new BorderPane(); - wrap.setTop(menuBar); - wrap.setCenter(vert); - Scene scene = new Scene(wrap, 900, 800); - scene.getStylesheets().add("gui.css"); - stage.setScene(scene); - updateTitle(); - updateLanguageMenu(); - updateThemeMenu(); - stage.show(); - Platform.runLater(() -> updateHTML()); - } - - /** - * Update the ui that need to be refreshed for the new configuration. - * - * @param configuration - * New configuration to load. - */ - private void setConfiguration(Configuration configuration) { - if(configuration.getLanguages().isEmpty()) { - error("Configuration load error", - "The configuration has no languages.", - "Use a config that has a specified language"); - return; - } - Language language = configuration.getLanguages().get(0); - if(language.getThemes().isEmpty()) { - error("Configuration load error", - "The configuration has no themes.", - "Use a config that has a specified theme"); - return; - } - Theme theme = language.getThemes().get(0); - helper = new ConfigHelper(configuration, language, theme); - updateCSS(); - updateRulesPane(); - updateLanguageMenu(); - updateThemeMenu(); - updateHTML(); - } - - /** - * Update the language menu to show the currently supported languages. - */ - private void updateLanguageMenu() { - mnLang.getItems().clear(); - List sorted = helper.getConfiguration().getLanguages() - .stream().sorted().collect(Collectors.toList()); - for(Language language : sorted) { - // Skip language if it has no theme - if(language.getThemes().isEmpty()) - continue; - // Create menu-item that sets the current language - String name = language.getName(); - name = name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase(); - MenuItem miLang = new MenuItem(name); - mnLang.getItems().add(miLang); - // Disable if the language is the currently active one - if(language == helper.getLanguage()) { - miLang.setDisable(true); - } else { - miLang.setOnAction(e -> { - helper.setLanguage(language); - helper.setTheme(language.getThemes().get(0)); - updateCSS(); - updateHTML(); - updateLanguageMenu(); - updateThemeMenu(); - updateRulesPane(); - updateThemeMenu(); - updateTitle(); - }); - } - } - mnLang.getItems().add(new SeparatorMenuItem()); - MenuItem mnNew = new MenuItem("New Language..."); - mnNew.setOnAction(e -> { - TextField txtName = new TextField(); - txtName.setPromptText("Name"); - ValidationSupport valiation = new ValidationSupport(); - Platform.runLater(() -> { - valiation.registerValidator(txtName, createEmptyValidator("Name cannot be empty")); - }); - // Create dialog - Dialog dialog = new Dialog<>(); - dialog.setTitle("New Language"); - valiation.invalidProperty().addListener((ob, o, n) -> { - dialog.getDialogPane().lookupButton(ButtonType.OK).setDisable(n); - }); - dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); - dialog.getDialogPane().setContent(txtName); - dialog.setResultConverter(dialogButton -> { - if(dialogButton == ButtonType.OK) { - return txtName.getText(); - } - return null; - }); - Platform.runLater(() -> txtName.requestFocus()); - Optional result = dialog.showAndWait(); - if(result.isPresent()) { - Language language = new Language(result.get()); - Theme theme = new Theme(); - theme.setName("default"); - language.addTheme(theme); - helper.getConfiguration().addLanguage(language); - helper.setLanguage(language); - helper.setTheme(theme); - updateCSS(); - updateHTML(); - updateRulesPane(); - updateLanguageMenu(); - updateThemeMenu(); - updateTitle(); - } - }); - mnLang.getItems().add(mnNew); - } - - /** - * Update te theme menu to show the currently supported themes for the language. - */ - private void updateThemeMenu() { - mnTheme.getItems().clear(); - for (Theme theme : helper.getLanguage().getThemes()) { - // Create menu-item that sets the current theme - String name = theme.getName(); - name = name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase(); - MenuItem miTheme = new MenuItem(name); - mnTheme.getItems().add(miTheme); - // Disable if the theme is the currently active one - if(theme == helper.getTheme()) { - miTheme.setDisable(true); - } else { - miTheme.setOnAction(e -> { - helper.setTheme(theme); - updateRulesPane(); - updateThemeMenu(); - updateCSS(); - updateHTML(); - }); - } - } - mnTheme.getItems().add(new SeparatorMenuItem()); - MenuItem mnNew = new MenuItem("New Theme..."); - mnNew.setOnAction(e -> { - TextField txtName = new TextField(); - txtName.setPromptText("Name"); - ValidationSupport valiation = new ValidationSupport(); - Platform.runLater(() -> { - valiation.registerValidator(txtName, createEmptyValidator("Name cannot be empty")); - }); - // Create dialog - Dialog dialog = new Dialog<>(); - dialog.setTitle("New Theme"); - valiation.invalidProperty().addListener((ob, o, n) -> { - dialog.getDialogPane().lookupButton(ButtonType.OK).setDisable(n); - }); - dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); - dialog.getDialogPane().setContent(txtName); - dialog.setResultConverter(dialogButton -> { - if(dialogButton == ButtonType.OK) { - return txtName.getText(); - } - return null; - }); - Platform.runLater(() -> txtName.requestFocus()); - Optional result = dialog.showAndWait(); - if(result.isPresent()) { - Theme theme = new Theme(); - theme.setName(result.get()); - helper.getLanguage().addTheme(theme); - helper.setTheme(theme); - updateThemeMenu(); - updateCSS(); - updateHTML(); - } - }); - mnTheme.getItems().add(mnNew); - } - - /** - * Reset the config pane. Shows the rules of the currently active language. - */ - private void updateRulesPane() { - ListView view = new ListView<>(); - view.getItems().addAll(helper.getRules()); - view.setCellFactory(v -> new RuleCell()); - view.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); - GridPane gridRuleBtns = new GridPane(); - Button btnAdd = new Button("New Rule"); - Button btnRemove = new Button("Remove Selected"); - Button btnUp = new Button("Move Up"); - Button btnDown = new Button("Move Down"); - btnRemove.setDisable(true); - btnUp.setDisable(true); - btnDown.setDisable(true); - gridRuleBtns.add(btnAdd, 0, 0); - gridRuleBtns.add(btnRemove, 1, 0); - gridRuleBtns.add(btnUp, 2, 0); - gridRuleBtns.add(btnDown, 3, 0); - view.getSelectionModel().selectedItemProperty().addListener((ob, o, n) -> { - btnRemove.setDisable(n == null); - // Moving - int i = view.getSelectionModel().getSelectedIndex(); - btnUp.setDisable(n == null || i == 0); - btnDown.setDisable(n == null || i == view.getItems().size() - 1); - }); - view.getItems().addListener((ListChangeListener) c -> { - helper.getRules().clear(); - for(Rule rule : view.getItems()) - helper.addRule(rule); - updateCSS(); - updateHTML(); - }); - view.setOnMouseClicked(e -> { - if(e.getButton().equals(MouseButton.PRIMARY) && e.getClickCount() == 2){ - Rule selected = view.getSelectionModel().getSelectedItem(); - // Inputs - TextField txtName = new TextField(selected.getName()); - txtName.setPromptText("Name"); - TextField txtRegex = new TextField(selected.getPattern()); - txtRegex.setPromptText("Regex pattern"); - ValidationSupport valiation = new ValidationSupport(); - Platform.runLater(() -> { - valiation.registerValidator(txtName, createEmptyValidator("Name cannot be empty")); - valiation.registerValidator(txtRegex, createPredicateValidator(s -> { - try { - new Pattern(txtRegex.getText()); - return true; - } catch(Exception ex) { - return false; - } - }, "Regex pattern must compile with JRegex")); - }); - // Setup grid - GridPane grid = new GridPane(); - grid.setHgap(10); - grid.setVgap(10); - grid.setPadding(new Insets(20, 150, 10, 10)); - grid.add(new Label("Name:"), 0, 0); - grid.add(txtName, 1, 0); - grid.add(new Label("Regex:"), 0, 1); - grid.add(txtRegex, 1, 1); - Platform.runLater(() -> txtName.requestFocus()); - // Create dialog - Dialog> dialog = new Dialog<>(); - dialog.setTitle("Edit Regex Rule"); - valiation.invalidProperty().addListener((ob, o, n) -> { - dialog.getDialogPane().lookupButton(ButtonType.OK).setDisable(n); - }); - dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); - dialog.getDialogPane().setContent(grid); - dialog.setResultConverter(dialogButton -> { - if(dialogButton == ButtonType.OK) { - return new Pair<>(txtName.getText(), txtRegex.getText()); - } - return null; - }); - Optional> result = dialog.showAndWait(); - if(result.isPresent()) { - Pair pair = result.get(); - String old = selected.getName(); - String rename = pair.getKey(); - if (!old.equals(rename)) { - // Rename the rule - selected.setName(rename); - helper.getLanguage().getThemes().forEach(theme -> { - theme.getStyles().stream() - .filter(prop -> prop.getTargetRule().equals(old)) - .forEach(prop -> prop.setTargetRule(rename)); - }); - } - // Update pattern and other ui views - selected.setPattern(pair.getValue()); - updateCSS(); - updateHTML(); - updateRulesPane(); - } - } - }); - btnAdd.setOnAction(e -> { - // Inputs - TextField txtName = new TextField(); - txtName.setPromptText("Name"); - TextField txtRegex = new TextField(); - txtRegex.setPromptText("Regex pattern"); - ValidationSupport valiation = new ValidationSupport(); - Platform.runLater(() -> { - valiation.registerValidator(txtName, createEmptyValidator("Name cannot be empty")); - valiation.registerValidator(txtRegex, createPredicateValidator(s -> { - try { - new Pattern(txtRegex.getText()); - return true; - } catch(Exception ex) { - return false; - } - }, "Regex pattern must compile with JRegex")); - }); - // Setup grid - GridPane grid = new GridPane(); - grid.setHgap(10); - grid.setVgap(10); - grid.setPadding(new Insets(20, 150, 10, 10)); - grid.add(new Label("Name:"), 0, 0); - grid.add(txtName, 1, 0); - grid.add(new Label("Regex:"), 0, 1); - grid.add(txtRegex, 1, 1); - Platform.runLater(() -> txtName.requestFocus()); - // Create dialog - Dialog> dialog = new Dialog<>(); - dialog.setTitle("New Regex Rule"); - valiation.invalidProperty().addListener((ob, o, n) -> { - dialog.getDialogPane().lookupButton(ButtonType.OK).setDisable(n); - }); - dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); - dialog.getDialogPane().setContent(grid); - dialog.setResultConverter(dialogButton -> { - if(dialogButton == ButtonType.OK) { - return new Pair<>(txtName.getText(), txtRegex.getText()); - } - return null; - }); - Optional> result = dialog.showAndWait(); - if(result.isPresent()) { - Pair pair = result.get(); - Rule rule = new Rule(); - rule.setName(pair.getKey()); - rule.setPattern(pair.getValue()); - view.getItems().add(rule); - } - }); - btnRemove.setOnAction(e -> view.getItems().remove(view.getSelectionModel() - .getSelectedIndex())); - btnUp.setOnAction(e -> { - int i = view.getSelectionModel().getSelectedIndex(); - previewLock = true; - Collections.swap(helper.getRules(), i, i - 1); - Collections.swap(view.getItems(), i, i - 1); - view.getSelectionModel().select(i - 1); - previewLock = false; - }); - btnDown.setOnAction(e -> { - int i = view.getSelectionModel().getSelectedIndex() + 1; - previewLock = true; - Collections.swap(helper.getRules(), i, i - 1); - Collections.swap(view.getItems(), i, i - 1); - view.getSelectionModel().select(i); - previewLock = false; - }); - patternsPane.setCenter(view); - patternsPane.setBottom(gridRuleBtns); - } - - /** - * Update title to match current language. - */ - private void updateTitle() { - String name = helper.getLanguage().getName(); - name = name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase(); - stage.setTitle(name + "2Html"); - } - - /** - * Update CSS text. - */ - private void updateCSS() { - txtCSS.setText(helper.getPatternCSS() + BASE_CSS); - } - - /** - * Generate and update HTML output / preview. - */ - private void updateHTML() { - if(previewLock) { - return; - } - String text = txtInput.getText().replace("\t", " "); - String html = helper.convert(text, inline); - String style = txtCSS.getText(); - StringBuilder sbWeb = new StringBuilder(); - sbWeb.append(""); - if (!inline) { - sbWeb.append(""); - } - sbWeb.append(""); - sbWeb.append(html); - sbWeb.append(""); - browser.getEngine().loadContent(sbWeb.toString()); - txtHTML.setText(html); - } - - /** - * Show error dialog and then exit. - * - * @param title - * Dialog title. - * @param header - * Message header. - * @param content - * Message content. - */ - private static void fatal(String title, String header, String content) { - error(title, header, content); - System.exit(-1); - } - - /** - * Show error dialog. - * - * @param title - * Dialog title. - * @param header - * Message header. - * @param content - * Message content. - */ - private static void error(String title, String header, String content) { - Alert alert = new Alert(Alert.AlertType.ERROR); - alert.setTitle(title); - alert.setHeaderText(header); - alert.setContentText(content); - alert.showAndWait(); - } - } diff --git a/src/main/java/me/coley/c2h/config/ConfigHelper.java b/src/main/java/me/coley/c2h/config/ConfigHelper.java deleted file mode 100644 index b92b0d9..0000000 --- a/src/main/java/me/coley/c2h/config/ConfigHelper.java +++ /dev/null @@ -1,201 +0,0 @@ -package me.coley.c2h.config; - -import jregex.Matcher; -import jregex.Pattern; -import lombok.Getter; -import lombok.Setter; -import me.coley.c2h.Code2Html; -import me.coley.c2h.config.model.*; -import me.coley.c2h.util.Regex; - -import java.util.List; -import java.util.Map; - -import static org.apache.commons.text.StringEscapeUtils.escapeHtml4; - -/** - * Utility to make working with the configuration object easier. - * - * @author Matt - */ -public class ConfigHelper { - @Getter - @Setter - private Configuration configuration; - @Getter - @Setter - private Language language; - @Getter - @Setter - private Theme theme; - - public ConfigHelper(Configuration configuration, Language language, Theme theme) { - if(configuration == null) - throw new IllegalStateException("Configuration cannot be null"); - if(language == null) - throw new IllegalStateException("Language cannot be null"); - if(theme == null) - throw new IllegalStateException("Theme cannot be null"); - this.configuration = configuration; - this.language = language; - this.theme = theme; - } - - /** - * Append regex pattern. - * - * @param name - * Name of the pattern. - * @param regex - * Pattern string. - */ - public void addRule(String name, String regex) { - Rule rule = new Rule(); - rule.setName(name); - rule.setPattern(regex); - addRule(rule); - } - - /** - * Append regex pattern. - * - * @param rule - * Pattern object. - */ - public void addRule(Rule rule) { - language.addRule(rule); - } - - /** - * @param text - * Text to convert into HTML. - * - * @param inline - * @return HTML of the text with matched attributes of the {@link #language current language}. - */ - public String convert(String text, boolean inline) { - Matcher matcher = getPattern().matcher(text); - StringBuilder sb = new StringBuilder(); - int lastEnd = 0; - while(matcher.find()) { - int start = matcher.start(); - int end = matcher.end(); - // append text not matched - if(start > lastEnd) { - String unmatched = escapeHtml4(text.substring(lastEnd, start)); - sb.append(unmatched); - } - // append match - String matched = escapeHtml4(text.substring(start, end)); - if (inline) { - String styleRules = getInlineStyleFromGroup(matcher); - sb.append("" + matched + ""); - } else { - String styleClass = getClassFromGroup(matcher); - sb.append("" + matched + ""); - } - lastEnd = end; - } - // Append ending text not matched - sb.append(escapeHtml4(text.substring(lastEnd))); - // Apply line formatting to each line - StringBuilder fmt = new StringBuilder(); - if(inline) { - StringBuilder sbLineStyle = new StringBuilder(); - StringBuilder sbLinePreStyle = new StringBuilder(); - Regex.getCssProperties(Code2Html.BASE_CSS, "pre .line").forEach((key, value) -> { - sbLineStyle.append(key + ":" + value + ";"); - }); - Regex.getCssProperties(Code2Html.BASE_CSS, "pre .line::before").forEach((key, value) -> { - sbLinePreStyle.append(key + ":" + value + ";"); - }); - int lineNum = 1; - for(String line : sb.toString().split("\n")) - fmt.append("" + (lineNum++) + "" + line + "\n"); - // Wrap in pre tags and slap it in an HTML page - StringBuilder sbPreStyle = new StringBuilder(); - Regex.getCssProperties(Code2Html.BASE_CSS, "pre").forEach((key, value) -> { - sbPreStyle.append(key + ":" + value + ";"); - }); - return "
" + fmt.toString() + "
"; - } else { - for(String line : sb.toString().split("\n")) - fmt.append("" + line + "\n"); - // Wrap in pre tags and slap it in an HTML page - return "
" + fmt.toString() + "
"; - } - } - - /** - * @return Compiled regex pattern from {@link #getRules() all existing rules}. - */ - public Pattern getPattern() { - if (getRules().isEmpty()) { - return new Pattern("({EMPTY}EMPTY)"); - } - StringBuilder sb = new StringBuilder(); - for(Rule rule : getRules()) - sb.append("({" + rule.getPatternGroupName() + "}" + rule.getPattern() + ")|"); - return new Pattern(sb.substring(0, sb.length() - 1)); - } - - /** - * @return CSS for patterns. - */ - public String getPatternCSS() { - StringBuilder sb = new StringBuilder(); - sb.append("/* =========================== */\n"); - sb.append("/* Custom element types */\n"); - sb.append("/* =========================== */\n"); - for(Rule rule : getRules()) { - sb.append("." + rule.getName() + " {"); - theme.getStylesForRule(rule.getName()).forEach(style -> { - sb.append("\n\t" + style.getKey() + ": " + style.getValue() + ";"); - }); - sb.append("\n}\n"); - } - return sb.toString(); - } - - /** - * @return List of added rules. - */ - public List getRules() { - return language.getRules(); - } - - /** - * Fetch the CSS class name to use based on the matched group. - * - * @param matcher - * Matcher that has found a group. - * - * @return CSS class name (Raw name of regex rule) - */ - public String getClassFromGroup(Matcher matcher) { - for(Rule rule : getRules()) - if(matcher.group(rule.getPatternGroupName()) != null) - return rule.getName(); - return null; - } - - /** - * Fetch the CSS style to be used by the matched group. - * - * @param matcher - * Matcher that has found a group. - * - * @return CSS inline style. - */ - public String getInlineStyleFromGroup(Matcher matcher) { - for(Rule rule : getRules()) - if(matcher.group(rule.getPatternGroupName()) != null) { - StringBuilder sbStyle = new StringBuilder(); - theme.getStylesForRule(rule.getName()) - .forEach(style -> - sbStyle.append(style.getKey() + ":" + style.getValue() + ";")); - return sbStyle.toString(); - } - return ""; - } -} diff --git a/src/main/java/me/coley/c2h/config/Exporter.java b/src/main/java/me/coley/c2h/config/Exporter.java index 1fde491..f5256e4 100644 --- a/src/main/java/me/coley/c2h/config/Exporter.java +++ b/src/main/java/me/coley/c2h/config/Exporter.java @@ -1,29 +1,44 @@ package me.coley.c2h.config; import me.coley.c2h.config.model.Configuration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import javax.xml.bind.*; -import java.io.IOException; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; import java.io.OutputStream; /** * Configuration exporter. * * @author Geoff Hayward - * @author Matt + * @author Matt Coley */ public class Exporter { - public static String toString(Configuration configuration) throws JAXBException { + private static final Logger logger = LoggerFactory.getLogger(Exporter.class); + + /** + * @param configuration + * Instance to export. + * + * @return XML representing the configuration. + * + * @throws JAXBException + * When the conversion fails. + */ + public static String toXML(Configuration configuration) throws JAXBException { + logger.debug("Exporting Configuration to XML: {}", configuration); // Create marshaller JAXBContext context = JAXBContext.newInstance(Configuration.class); Marshaller m = context.createMarshaller(); m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); - // Export config to outputstream and return the toString value. + // Export config to OutputStream and return the toString() value. OutputStream output = new OutputStream() { private final StringBuilder string = new StringBuilder(); @Override - public void write(int b) throws IOException { + public void write(int b) { this.string.append((char) b); } diff --git a/src/main/java/me/coley/c2h/config/Importer.java b/src/main/java/me/coley/c2h/config/Importer.java index fe3c51c..caed3a5 100644 --- a/src/main/java/me/coley/c2h/config/Importer.java +++ b/src/main/java/me/coley/c2h/config/Importer.java @@ -1,13 +1,20 @@ package me.coley.c2h.config; import me.coley.c2h.config.model.Configuration; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import javax.xml.bind.*; -import java.io.*; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; import java.net.URI; -import java.nio.file.*; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import static java.nio.charset.StandardCharsets.UTF_8; @@ -17,7 +24,8 @@ * @author Geoff Hayward */ public final class Importer { - private final static String DEFAULT_CONF = "default-config.c2h"; + private static final Logger logger = LoggerFactory.getLogger(Importer.class); + private final static String DEFAULT_CONF = "default-config.xml"; /** * @param path @@ -31,10 +39,11 @@ public final class Importer { * @throws IOException * Thrown if the file could not be read from. */ - public static Configuration importFromFile(String path) throws JAXBException, IOException { + public static Configuration importFromFile(Path path) throws JAXBException, IOException { + logger.debug("Importing configuration from path: {}", path); JAXBContext context = JAXBContext.newInstance(Configuration.class); Unmarshaller um = context.createUnmarshaller(); - return (Configuration) um.unmarshal(new StringReader(FileUtils.readFileToString(new File(path), UTF_8))); + return (Configuration) um.unmarshal(new StringReader(Files.readString(path))); } /** @@ -48,6 +57,7 @@ public static Configuration importFromFile(String path) throws JAXBException, IO * This typically means the file is formatted incorrectly. */ public static Configuration importFromText(String text) throws JAXBException { + logger.debug("Importing configuration from text"); JAXBContext context = JAXBContext.newInstance(Configuration.class); Unmarshaller um = context.createUnmarshaller(); return (Configuration) um.unmarshal(new StringReader(text)); @@ -63,17 +73,29 @@ public static Configuration importFromText(String text) throws JAXBException { * Thrown if the file could not be read from. */ public static Configuration importDefault() throws JAXBException, IOException { + logger.debug("Importing configuration from default location"); ClassLoader classloader = Importer.class.getClassLoader(); - String uriPath = classloader.getResource(DEFAULT_CONF).toExternalForm(); + URL resourceUrl = classloader.getResource(DEFAULT_CONF); + if (resourceUrl == null) { + logger.error("Could not load default config from path: {}", DEFAULT_CONF); + throw new IOException("Resource not found: " + DEFAULT_CONF); + } + String uriPath = resourceUrl.toExternalForm(); URI uri = URI.create(uriPath); - switch(uri.getScheme()) { - case "file": + switch (uri.getScheme()) { + case "file" -> { Path path = Paths.get(uri); - return importFromFile(path.toString()); - case "jar": - InputStream in = classloader.getResourceAsStream(DEFAULT_CONF); - byte[] data = IOUtils.toByteArray(in); - return importFromText(new String(data, UTF_8)); + return importFromFile(path); + } + case "jar" -> { + try (InputStream in = classloader.getResourceAsStream(DEFAULT_CONF)) { + if (in == null) { + throw new IOException("Resource not found: " + DEFAULT_CONF); + } + byte[] data = in.readAllBytes(); + return importFromText(new String(data, UTF_8)); + } + } } throw new IOException("Default configuration location unknown"); } diff --git a/src/main/java/me/coley/c2h/config/model/Configuration.java b/src/main/java/me/coley/c2h/config/model/Configuration.java index bd68d1a..410bda1 100644 --- a/src/main/java/me/coley/c2h/config/model/Configuration.java +++ b/src/main/java/me/coley/c2h/config/model/Configuration.java @@ -1,8 +1,9 @@ package me.coley.c2h.config.model; -import lombok.Getter; - -import javax.xml.bind.annotation.*; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; import java.util.ArrayList; import java.util.List; @@ -14,9 +15,15 @@ @XmlRootElement @XmlAccessorType(XmlAccessType.FIELD) public class Configuration { - @Getter @XmlElement(name = "language") - private List languages = new ArrayList<>(); + private final List languages = new ArrayList<>(); + + /** + * @return Languages supported in the config instance. + */ + public List getLanguages() { + return languages; + } /** * Add a language to the config. diff --git a/src/main/java/me/coley/c2h/config/model/Language.java b/src/main/java/me/coley/c2h/config/model/Language.java index 07929fb..d919a72 100644 --- a/src/main/java/me/coley/c2h/config/model/Language.java +++ b/src/main/java/me/coley/c2h/config/model/Language.java @@ -1,11 +1,12 @@ package me.coley.c2h.config.model; -import lombok.EqualsAndHashCode; -import lombok.Getter; - -import javax.xml.bind.annotation.*; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; import java.util.ArrayList; import java.util.List; +import java.util.Objects; /** * A collection of rules that match against a language's feature set and themes to apply distinct @@ -13,26 +14,10 @@ * * @author Geoff Hayward */ -@EqualsAndHashCode @XmlAccessorType(XmlAccessType.FIELD) public class Language implements Comparable { - /** - * Rules for matching against language features. - */ - @Getter @XmlElement(name = "rule") - private List rules = new ArrayList<>(); - /** - * Theme to apply to language. Determines which set of CSS styles are applied to matches. - */ - @Getter - @XmlElementWrapper(name = "themes") - @XmlElement(name = "theme") - private List themes = new ArrayList<>(); - /** - * Language identifier. - */ - @Getter + private final List rules = new ArrayList<>(); @XmlAttribute private String name; @@ -56,29 +41,60 @@ public void addRule(Rule rule) { } /** - * Add a theme to the language. + * @param name + * Name of rule to find. * - * @param theme - * Theme to add. + * @return Rule by name, or {@code null} when there is no match. + */ + public Rule getRule(String name) { + return rules.stream() + .filter(rule -> rule.getName().equals(name)) + .findFirst() + .orElse(null); + } + + /** + * @return Rules for matching against language features. + */ + public List getRules() { + return rules; + } + + /** + * @return Language identifier. */ - public void addTheme(Theme theme) { - this.themes.add(theme); + public String getName() { + return name; } /** * @param name - * Theme identifier. - * - * @return Theme matching the given name. + * Language identifier. */ - public Theme findTheme(String name) { - return themes.stream() - .filter(t -> t.getName().equalsIgnoreCase(name)) - .findFirst().orElse(null); + public void setName(String name) { + this.name = name; } @Override public int compareTo(Language other) { return getName().toLowerCase().compareTo(other.getName().toLowerCase()); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Language language = (Language) o; + + if (!Objects.equals(rules, language.rules)) return false; + return Objects.equals(name, language.name); + } + + @Override + public int hashCode() { + int result = rules.hashCode(); + result = 31 * result + (name != null ? name.hashCode() : 0); + return result; + } } diff --git a/src/main/java/me/coley/c2h/config/model/Rule.java b/src/main/java/me/coley/c2h/config/model/Rule.java index bcfda5e..ee1816d 100644 --- a/src/main/java/me/coley/c2h/config/model/Rule.java +++ b/src/main/java/me/coley/c2h/config/model/Rule.java @@ -1,27 +1,26 @@ package me.coley.c2h.config.model; -import lombok.*; - import javax.xml.bind.annotation.*; +import java.util.ArrayList; +import java.util.List; /** * Language rule that matches against a feature of a language using regex. * * @author Geoff Hayward */ -@Data @XmlAccessorType(XmlAccessType.FIELD) public class Rule { - /** - * The rule identifier. - */ - @XmlAttribute + @XmlAttribute(required = true) private String name; - /** - * The regex pattern. - */ - @XmlElement + @XmlElement(required = true) private String pattern; + @XmlElementWrapper(name = "style") + @XmlElement(name = "entry") + private List style = new ArrayList<>(); + @XmlElement(name = "rule") + @XmlElementWrapper(name = "subrules") + private List subrules = new ArrayList<>(); /** * @return Name as proper regex group title. @@ -30,6 +29,91 @@ public String getPatternGroupName() { return sterilize(name); } + /** + * @return Name literal / rule identifier. + * + * @see #getPatternGroupName() + */ + public String getName() { + return name; + } + + /** + * @param name + * Name literal / rule identifier. + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return Regex pattern string. + */ + public String getPattern() { + return pattern; + } + + /** + * @param pattern + * Regex pattern string. + */ + public void setPattern(String pattern) { + this.pattern = pattern; + } + + /** + * @return List of style properties to apply to matched content. + */ + public List getStyle() { + return style; + } + + /** + * @param style + * List of style properties to apply to matched content. + */ + public void setStyle(List style) { + this.style = style; + } + + /** + * @param property + * Style property to remove. + */ + public void removeStyle(StyleProperty property) { + style.remove(property); + } + + /** + * @param property + * Style property to add. + */ + public void addStyle(StyleProperty property) { + // Remove prior entry if same 'key' + style.removeIf(p -> p.getKey().equals(property.getKey())); + style.add(property); + } + + /** + * @return Rules to apply within this rule's matched bounds. + */ + public List getSubrules() { + return subrules; + } + + /** + * @param subrules + * Rules to apply within this rule's matched bounds. + */ + public void setSubrules(List subrules) { + this.subrules = subrules; + } + + @Override + public String toString() { + return getName() + ": " + getPattern(); + } + /** * @param name * Original name. diff --git a/src/main/java/me/coley/c2h/config/model/StyleProperty.java b/src/main/java/me/coley/c2h/config/model/StyleProperty.java index 808b27c..77af637 100644 --- a/src/main/java/me/coley/c2h/config/model/StyleProperty.java +++ b/src/main/java/me/coley/c2h/config/model/StyleProperty.java @@ -1,30 +1,90 @@ package me.coley.c2h.config.model; -import lombok.*; - import javax.xml.bind.annotation.*; +import java.util.List; +import java.util.Objects; /** * CSS properties to apply to some language's rule. * * @author Geoff Hayward */ -@Data @XmlAccessorType(XmlAccessType.FIELD) public class StyleProperty { + @XmlAttribute(required = true) + private String key; + @XmlAttribute + private String value; + @XmlElementWrapper(name = "style") + @XmlElement(name = "entry") + private List style; + + public StyleProperty() {} + /** - * The target rule this style applies to. + * @param key + * CSS property key. + * @param value + * CSS property value. */ - @XmlAttribute - private String targetRule; + public StyleProperty(String key, String value) { + this.key = key; + this.value = value; + } + /** - * The CSS property name. + * @return CSS property key. */ - @XmlAttribute - private String key; + public String getKey() { + return key; + } + /** - * The CSS property value. + * @param key + * CSS property key. */ - @XmlAttribute - private String value; + public void setKey(String key) { + this.key = key; + } + + /** + * @return CSS property value. + */ + public String getValue() { + return value; + } + + /** + * @param value + * CSS property value. + */ + public void setValue(String value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + StyleProperty property = (StyleProperty) o; + + if (!Objects.equals(key, property.key)) return false; + return Objects.equals(value, property.value); + } + + @Override + public int hashCode() { + int result = (key != null ? key.hashCode() : 0); + result = 31 * result + (value != null ? value.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "StyleProperty{" + + "key='" + key + '\'' + + ", value='" + value + '\'' + + '}'; + } } diff --git a/src/main/java/me/coley/c2h/config/model/Theme.java b/src/main/java/me/coley/c2h/config/model/Theme.java deleted file mode 100644 index 65e2d44..0000000 --- a/src/main/java/me/coley/c2h/config/model/Theme.java +++ /dev/null @@ -1,66 +0,0 @@ -package me.coley.c2h.config.model; - -import lombok.Data; - -import javax.xml.bind.annotation.*; -import java.util.*; -import java.util.stream.Collectors; - -/** - * Collection of styles that will be applied to a language's rule-set. - * - * @author Geoff Hayward - */ -@Data -@XmlAccessorType(XmlAccessType.FIELD) -public class Theme { - /** - * Name of the theme. - */ - @XmlAttribute - private String name; - /** - * Styles for the rules of the theme. - */ - @XmlElement(name = "style") - private List styles = new ArrayList<>(); - - - /** - * @param rule - * Target rule name. - * - * @return List of style properties that are associated with the given rule. - */ - public List getStylesForRule(String rule) { - return styles.stream() - .filter(sr -> sr.getTargetRule().equals(rule)) - .collect(Collectors.toList()); - } - - /** - * Update a style property belonging to the given rule. - * - * @param rule - * Target rule name. - * @param key - * Style property key. - * @param value - * Style property value to set. - */ - public void update(String rule, String key, String value) { - Collection matched = styles.stream() - .filter(sr -> sr.getTargetRule().equals(rule) && - sr.getKey().equals(key)) - .collect(Collectors.toList()); - if(matched.isEmpty()) { - StyleProperty property = new StyleProperty(); - property.setTargetRule(rule); - property.setKey(key); - property.setValue(value); - styles.add(property); - } else { - matched.forEach(property -> property.setValue(value)); - } - } -} diff --git a/src/main/java/me/coley/c2h/ui/ConfigUpdateListener.java b/src/main/java/me/coley/c2h/ui/ConfigUpdateListener.java new file mode 100644 index 0000000..f6fc1fc --- /dev/null +++ b/src/main/java/me/coley/c2h/ui/ConfigUpdateListener.java @@ -0,0 +1,21 @@ +package me.coley.c2h.ui; + +import me.coley.c2h.config.model.Configuration; +import me.coley.c2h.config.model.Language; + +/** + * Listener to handle changes to {@link Configuration} / {@link Language} selection. + * + * @author Matt Coley + */ +public interface ConfigUpdateListener { + /** + * @param config New config instance. + */ + void onConfigChanged(Configuration config); + + /** + * @param language Target language from the current config instance. + */ + void onTargetLanguageSet(Language language); +} diff --git a/src/main/java/me/coley/c2h/ui/InputUpdateListener.java b/src/main/java/me/coley/c2h/ui/InputUpdateListener.java new file mode 100644 index 0000000..6c17aac --- /dev/null +++ b/src/main/java/me/coley/c2h/ui/InputUpdateListener.java @@ -0,0 +1,14 @@ +package me.coley.c2h.ui; + +/** + * Listener to handle text inputs from {@link me.coley.c2h.ui.pane.InputPane} + * + * @author Matt Coley + */ +public interface InputUpdateListener { + /** + * @param source + * Input text. + */ + void onInput(String source); +} diff --git a/src/main/java/me/coley/c2h/ui/RootUI.java b/src/main/java/me/coley/c2h/ui/RootUI.java new file mode 100644 index 0000000..245911c --- /dev/null +++ b/src/main/java/me/coley/c2h/ui/RootUI.java @@ -0,0 +1,60 @@ +package me.coley.c2h.ui; + +import com.panemu.tiwulfx.control.dock.DetachableTabPane; +import javafx.application.Application; +import javafx.geometry.Orientation; +import javafx.scene.Scene; +import javafx.scene.control.SplitPane; +import javafx.stage.Stage; +import me.coley.c2h.config.Importer; +import me.coley.c2h.config.model.Configuration; +import me.coley.c2h.ui.pane.ConfigPane; +import me.coley.c2h.ui.pane.InputPane; +import me.coley.c2h.ui.pane.OutputPane; + +/** + * Root UI layout. + * + * @author Matt Coley + */ +public class RootUI extends Application { + private static final int WIDTH = 1080; + private static final int HEIGHT = 720; + private final InputPane inputPane; + private final OutputPane outputPane; + private final ConfigPane configPane; + + public RootUI() { + Configuration configuration; + try { + configuration = Importer.importDefault(); + } catch (Exception ex) { + throw new IllegalStateException("Cannot read default config", ex); + } + inputPane = new InputPane(); + outputPane = new OutputPane(); + configPane = new ConfigPane(configuration); + // Hook up listeners + inputPane.setListener(outputPane); + configPane.setListener(outputPane); + } + + @Override + public void start(Stage primaryStage) { + SplitPane wrapper = new SplitPane(); + wrapper.setOrientation(Orientation.VERTICAL); + DetachableTabPane top = new DetachableTabPane(); + DetachableTabPane center = new DetachableTabPane(); + top.addTab("Input", inputPane).setClosable(false); + top.addTab("Config", configPane).setClosable(false); + center.addTab("Output", outputPane).setClosable(false); + wrapper.getItems().addAll(top, center); + Scene scene = new Scene(wrapper); + scene.getStylesheets().add("gui.css"); + primaryStage.setWidth(WIDTH); + primaryStage.setHeight(HEIGHT); + primaryStage.setScene(scene); + primaryStage.setTitle("Code2HTML"); + primaryStage.show(); + } +} diff --git a/src/main/java/me/coley/c2h/ui/RuleCell.java b/src/main/java/me/coley/c2h/ui/RuleCell.java index bc3cd16..592d177 100644 --- a/src/main/java/me/coley/c2h/ui/RuleCell.java +++ b/src/main/java/me/coley/c2h/ui/RuleCell.java @@ -8,13 +8,16 @@ /** * ListCell for displaying language rules. * - * @author Matt + * @author Matt Coley */ public final class RuleCell extends ListCell { @Override protected void updateItem(Rule item, boolean empty) { + // TODO: Re-add this to the config menu + // - likely as a tree-cell since rules can be hierarchies now + super.updateItem(item, empty); - if(empty || item == null) { + if (empty || item == null) { setGraphic(null); setText(null); } else { diff --git a/src/main/java/me/coley/c2h/ui/WebScrollHelper.java b/src/main/java/me/coley/c2h/ui/WebScrollHelper.java new file mode 100644 index 0000000..2cea440 --- /dev/null +++ b/src/main/java/me/coley/c2h/ui/WebScrollHelper.java @@ -0,0 +1,72 @@ +package me.coley.c2h.ui; + +import javafx.scene.web.WebView; + +/** + * Utilities for {@link WebView} scrolling. + * + * @author miho + */ +public class WebScrollHelper { + /** + * Scrolls to the specified position. + * + * @param view + * web view that shall be scrolled + * @param x + * horizontal scroll value + * @param y + * vertical scroll value + */ + public static void scrollTo(WebView view, int x, int y) { + view.getEngine().executeScript("window.scrollTo(" + x + ", " + y + ")"); + } + + /** + * Returns the vertical scroll value, i.e. thumb position. + * This is equivalent to {@link javafx.scene.control.ScrollBar#getValue(). + * + * @param view + * + * @return vertical scroll value + */ + public static int getVScrollValue(WebView view) { + return (Integer) view.getEngine().executeScript("document.body.scrollTop"); + } + + /** + * Returns the horizontal scroll value, i.e. thumb position. + * This is equivalent to {@link javafx.scene.control.ScrollBar#getValue()}. + * + * @param view + * + * @return horizontal scroll value + */ + public static int getHScrollValue(WebView view) { + return (Integer) view.getEngine().executeScript("document.body.scrollLeft"); + } + + /** + * Returns the maximum vertical scroll value. + * This is equivalent to {@link javafx.scene.control.ScrollBar#getMax()}. + * + * @param view + * + * @return vertical scroll max + */ + public static int getVScrollMax(WebView view) { + return (Integer) view.getEngine().executeScript("document.body.scrollWidth"); + } + + /** + * Returns the maximum horizontal scroll value. + * This is equivalent to {@link javafx.scene.control.ScrollBar#getMax()}. + * + * @param view + * + * @return horizontal scroll max + */ + public static int getHScrollMax(WebView view) { + return (Integer) view.getEngine().executeScript("document.body.scrollHeight"); + } +} diff --git a/src/main/java/me/coley/c2h/ui/pane/ConfigPane.java b/src/main/java/me/coley/c2h/ui/pane/ConfigPane.java new file mode 100644 index 0000000..dd320fe --- /dev/null +++ b/src/main/java/me/coley/c2h/ui/pane/ConfigPane.java @@ -0,0 +1,153 @@ +package me.coley.c2h.ui.pane; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.layout.BorderPane; +import javafx.stage.FileChooser; +import me.coley.c2h.config.Exporter; +import me.coley.c2h.config.Importer; +import me.coley.c2h.config.model.Configuration; +import me.coley.c2h.config.model.Language; +import me.coley.c2h.ui.ConfigUpdateListener; + +import java.io.File; +import java.nio.file.Files; + +/** + * Configuration pane for tweaking the {@link Language} values in a {@link Configuration} instance. + * + * @author Matt Coley + */ +public class ConfigPane extends BorderPane { + private final ObjectProperty targetLanguage = new SimpleObjectProperty<>(); + private final ObjectProperty targetConfiguration = new SimpleObjectProperty<>(); + private ConfigUpdateListener listener; + + /** + * @param initialConfiguration + * Initial config state to pull from. + */ + public ConfigPane(Configuration initialConfiguration) { + if (initialConfiguration == null || initialConfiguration.getLanguages().isEmpty()) + throw new IllegalArgumentException("Must supply non-empty config"); + + targetLanguage.addListener((ob, old, cur) -> { + if (cur == null) { + setCenter(null); + } else { + setCenter(new LanguagePane(cur, () -> listener)); + } + + refreshMenu(); + if (listener != null) listener.onTargetLanguageSet(cur); + }); + + targetConfiguration.addListener((old, ob, cur) -> { + targetLanguage.setValue(cur.getLanguages().get(0)); + refreshMenu(); + if (listener != null) listener.onConfigChanged(cur); + }); + targetConfiguration.setValue(initialConfiguration); + } + + private void refreshMenu() { + setTop(generateMenu()); + } + + private Node generateMenu() { + Configuration config = targetConfiguration.getValue(); + Menu menuLanguages = new Menu("Language"); + { + for (Language language : config.getLanguages()) { + MenuItem item = new MenuItem(language.getName()); + item.setOnAction(e -> targetLanguage.setValue(language)); + menuLanguages.getItems().add(item); + } + + MenuItem itemNewLanguage = new MenuItem("Add new"); + itemNewLanguage.setOnAction(e -> { + TextInputDialog dialog = new TextInputDialog(); + dialog.setTitle("New language"); + dialog.showAndWait().ifPresent(name -> { + Language language = new Language(name); + config.addLanguage(language); + targetLanguage.setValue(language); + }); + }); + + MenuItem itemRenameLanguage = new MenuItem("Rename current"); + itemRenameLanguage.setOnAction(e -> { + TextInputDialog dialog = new TextInputDialog(); + dialog.setTitle("Rename language"); + dialog.showAndWait().ifPresent(name -> { + Language language = targetLanguage.getValue(); + language.setName(name); + refreshMenu(); + }); + }); + itemRenameLanguage.disableProperty().bind(targetLanguage.isNull()); + + + MenuItem itemRemoveLanguage = new MenuItem("Remove current"); + itemRemoveLanguage.setOnAction(e -> { + config.getLanguages().remove(targetLanguage.get()); + targetLanguage.set(null); + setCenter(null); + refreshMenu(); + }); + itemRemoveLanguage.disableProperty().bind(targetLanguage.isNull()); + + menuLanguages.getItems().addAll(new SeparatorMenuItem(), itemNewLanguage, itemRenameLanguage, itemRemoveLanguage); + } + + Menu menuFile = new Menu("File"); + { + MenuItem itemLoad = new MenuItem("Load config"); + MenuItem itemSave = new MenuItem("Save config"); + itemLoad.setOnAction(e -> { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Load config"); + File file = chooser.showOpenDialog(getScene().getWindow()); + if (file != null && file.exists()) { + try { + Configuration v = Importer.importFromFile(file.toPath()); + targetConfiguration.setValue(v); + } catch (Exception ex) { + new Alert(Alert.AlertType.ERROR, ex.getMessage()).show(); + } + } + }); + itemSave.setOnAction(e -> { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Save config"); + chooser.setInitialFileName("config.xml"); + File file = chooser.showSaveDialog(getScene().getWindow()); + if (file != null) { + try { + Files.writeString(file.toPath(), Exporter.toXML(config)); + } catch (Exception ex) { + new Alert(Alert.AlertType.ERROR, ex.getMessage()).show(); + } + } + }); + menuFile.getItems().addAll(itemLoad, itemSave); + } + return new MenuBar(menuFile, menuLanguages); + } + + /** + * @param listener + * Listener to receive updates from changes to the configuration. + */ + public void setListener(ConfigUpdateListener listener) { + this.listener = listener; + + // When assigned, pass the config and initial language. + if (listener != null) { + listener.onConfigChanged(targetConfiguration.get()); + listener.onTargetLanguageSet(targetLanguage.get()); + } + } +} diff --git a/src/main/java/me/coley/c2h/ui/pane/InputPane.java b/src/main/java/me/coley/c2h/ui/pane/InputPane.java new file mode 100644 index 0000000..df4c2ef --- /dev/null +++ b/src/main/java/me/coley/c2h/ui/pane/InputPane.java @@ -0,0 +1,35 @@ +package me.coley.c2h.ui.pane; + +import javafx.scene.control.TextArea; +import javafx.scene.layout.BorderPane; +import me.coley.c2h.ui.InputUpdateListener; + +/** + * Input pane for source text to stylize. + * + * @author Matt Coley + */ +public class InputPane extends BorderPane { + private static final String INITIAL_TEXT = "// paste your code here"; + private InputUpdateListener listener; + private String lastText = INITIAL_TEXT; + + public InputPane() { + TextArea text = new TextArea(INITIAL_TEXT); + text.getStyleClass().add("mono"); + text.textProperty().addListener((o, old, current) -> { + lastText = current; + if (listener != null) { + listener.onInput(current); + } + }); + setCenter(text); + } + + public void setListener(InputUpdateListener listener) { + this.listener = listener; + + if (listener != null) + listener.onInput(lastText); + } +} diff --git a/src/main/java/me/coley/c2h/ui/pane/LanguagePane.java b/src/main/java/me/coley/c2h/ui/pane/LanguagePane.java new file mode 100644 index 0000000..8b8f92f --- /dev/null +++ b/src/main/java/me/coley/c2h/ui/pane/LanguagePane.java @@ -0,0 +1,175 @@ +package me.coley.c2h.ui.pane; + +import javafx.geometry.Insets; +import javafx.geometry.VPos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextField; +import javafx.scene.layout.*; +import me.coley.c2h.config.model.Language; +import me.coley.c2h.config.model.Rule; +import me.coley.c2h.config.model.StyleProperty; +import me.coley.c2h.ui.ConfigUpdateListener; +import me.coley.c2h.util.Regex; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Configuration pane for modifying contents of a {@link Language}. + * + * @author Matt Coley + */ +class LanguagePane extends BorderPane { + private final Supplier listenerProxy; + private final Language language; + + /** + * @param language + * Target language. + * @param listenerProxy + * Proxy to listener to call when changes are made. + */ + public LanguagePane(Language language, Supplier listenerProxy) { + this.language = language; + this.listenerProxy = listenerProxy; + + // TODO: + // - rule + // - add rule + // - remove rule + // - change name + + VBox box = new VBox(); + box.setFillWidth(true); + box.setPadding(new Insets(5)); + ScrollPane scrollPane = new ScrollPane(box); + for (Rule rule : language.getRules()) { + visit(pane -> box.getChildren().add(pane), rule); + } + + // Hack + box.prefWidthProperty().bind(scrollPane.widthProperty().subtract(15)); + + // Label title = new Label(language.getName()); + // title.getStyleClass().add("h1"); + // setTop(title); + setCenter(scrollPane); + } + + private void notifyChange() { + listenerProxy.get().onTargetLanguageSet(language); + } + + private void visit(Consumer placer, Rule rule) { + RulePane pane = new RulePane(rule); + + placer.accept(pane); + + List subrules = rule.getSubrules(); + if (!subrules.isEmpty()) { + VBox box = new VBox(); + box.setFillWidth(true); + box.setPadding(new Insets(10, 10, 10, 60)); + pane.setBottom(box); + + for (Rule subrule : subrules) { + visit(subpane -> box.getChildren().add(subpane), subrule); + } + } + } + + private class RulePane extends BorderPane { + public RulePane(Rule rule) { + // Title + Label title = new Label(rule.getName()); + title.getStyleClass().add("h2"); + setTop(title); + + // Regex + TextField patternInput = new TextField(rule.getPattern()); + patternInput.setMaxWidth(Integer.MAX_VALUE); + patternInput.getStyleClass().add("input"); + patternInput.textProperty().addListener((ob, old, cur) -> { + if (Regex.isValid(cur)) { + rule.setPattern(cur); + notifyChange(); + } + }); + + // Style + StyleGrid styleGrid = new StyleGrid(rule); + + // Layout + Label styleLabel = new Label("Styles: "); + GridPane grid = new GridPane(10, 10); + GridPane.setValignment(styleGrid, VPos.TOP); + ColumnConstraints alwaysGrow = new ColumnConstraints(); + ColumnConstraints neverGrow = new ColumnConstraints(); + alwaysGrow.setHgrow(Priority.ALWAYS); + neverGrow.setHgrow(Priority.NEVER); + grid.getColumnConstraints().addAll(neverGrow, alwaysGrow); + grid.add(title, 0, 0, 2, 1); + grid.addRow(1, new Label("Pattern: "), patternInput); + grid.addRow(2, styleLabel, styleGrid); + setCenter(grid); + } + } + + private class StyleGrid extends GridPane { + private StyleGrid(Rule rule) { + super(5, 5); + + ColumnConstraints alwaysGrow = new ColumnConstraints(); + ColumnConstraints neverGrow = new ColumnConstraints(); + alwaysGrow.setHgrow(Priority.ALWAYS); + neverGrow.setHgrow(Priority.NEVER); + getColumnConstraints().addAll( + alwaysGrow, + alwaysGrow, + neverGrow); + + + Button addStyle = new Button("Add property"); + addStyle.setOnAction(e -> { + StyleProperty style = new StyleProperty("key", "value"); + rule.getStyle().add(style); + addStyle(2, rule, style); + }); + + addRow(0, new Label("Property"), new Label("Value")); + addRow(1, addStyle); + + for (StyleProperty style : rule.getStyle()) { + int row = getRowCount(); + addStyle(row, rule, style); + } + } + + private void addStyle(int row, Rule rule, StyleProperty style) { + Button delete = new Button("Delete"); + delete.setOnAction(e -> { + rule.getStyle().remove(style); + getChildren().removeIf(node -> GridPane.getRowIndex(node) == row); + notifyChange(); + }); + TextField keyField = new TextField(style.getKey()); + TextField valueField = new TextField(style.getValue()); + keyField.setMaxWidth(Integer.MAX_VALUE); + valueField.setMaxWidth(Integer.MAX_VALUE); + keyField.getStyleClass().add("input"); + valueField.getStyleClass().add("input"); + keyField.textProperty().addListener((ob, old, cur) -> { + style.setKey(cur); + notifyChange(); + }); + valueField.textProperty().addListener((ob, old, cur) -> { + style.setValue(cur); + notifyChange(); + }); + addRow(row, keyField, valueField, delete); + } + } +} diff --git a/src/main/java/me/coley/c2h/ui/pane/OutputPane.java b/src/main/java/me/coley/c2h/ui/pane/OutputPane.java new file mode 100644 index 0000000..e1ad364 --- /dev/null +++ b/src/main/java/me/coley/c2h/ui/pane/OutputPane.java @@ -0,0 +1,283 @@ +package me.coley.c2h.ui.pane; + +import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.geometry.Orientation; +import javafx.scene.control.CheckBox; +import javafx.scene.control.SplitPane; +import javafx.scene.control.TextArea; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.web.WebView; +import me.coley.c2h.config.model.Configuration; +import me.coley.c2h.config.model.Language; +import me.coley.c2h.ui.ConfigUpdateListener; +import me.coley.c2h.ui.InputUpdateListener; +import me.coley.c2h.ui.WebScrollHelper; +import me.coley.c2h.util.LanguageMatcher; + +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +/** + * Display pane showing: + *
    + *
  • HTML preview
  • + *
  • HTML source to copy
  • + *
      + *
    • Option to toggle between full HTML page and local {@code
      } block
    • + *
    • Option to toggle usage of CSS classes vs inline CSS
    • + *
    + *
  • CSS to copy & quickly modify
  • + *
+ * + * @author Matt Coley + */ +public class OutputPane extends BorderPane implements ConfigUpdateListener, InputUpdateListener { + public static String BASE_CSS = ""; + public static String BASE_JS = ""; + private final WebView htmlView = new WebView(); + private final TextArea htmlText = new TextArea(); + private final TextArea cssText = new TextArea(); + private final ExecutorService executorService = Executors.newSingleThreadExecutor(r -> { + // Must be daemon so app can shut down without lingering threads + Thread thread = new Thread(r); + thread.setDaemon(true); + return thread; + }); + private Future lastBuildRequest; + // Config + private final BooleanProperty inlineCss = new SimpleBooleanProperty(); + private final BooleanProperty fullHtml = new SimpleBooleanProperty(); + private final BooleanProperty includeJs = new SimpleBooleanProperty(); + private Configuration config; + private Language targetLanguage; + // Input + private String lastSource; + private volatile boolean isHtmlGenerationInProgress; + private volatile boolean isCssGenerationInProgress; + + static { + try (InputStream is = OutputPane.class.getResourceAsStream("/code.css")) { + if (is != null) BASE_CSS = new String(is.readAllBytes()); + } catch (Exception ex) { + throw new IllegalStateException("Missing 'code.css' resource, cannot infer default CSS"); + } + + try (InputStream is = OutputPane.class.getResourceAsStream("/code.js")) { + if (is != null) BASE_JS = new String(is.readAllBytes()); + } catch (Exception ex) { + throw new IllegalStateException("Missing 'code.js' resource, cannot infer default JS"); + } + } + + public OutputPane() { + // Outputs + htmlText.setEditable(false); + htmlText.getStyleClass().add("mono"); + cssText.getStyleClass().add("mono"); + htmlText.setWrapText(true); + cssText.setWrapText(true); + // Options + CheckBox btnFullHtml = new CheckBox("Include full HTML"); + CheckBox btnIncludeJs = new CheckBox("Include JS"); + CheckBox btnInline = new CheckBox("Use inline CSS"); + btnIncludeJs.disableProperty().bind(btnFullHtml.selectedProperty().not()); + cssText.disableProperty().bind(btnInline.selectedProperty()); + fullHtml.bind(btnFullHtml.selectedProperty()); + includeJs.bind(btnIncludeJs.selectedProperty()); + inlineCss.bind(btnInline.selectedProperty()); + // Layout + HBox options = new HBox(btnFullHtml, /*btnIncludeJs,*/ btnInline); + options.setSpacing(15); + BorderPane textOutputWrapper = new BorderPane(); + options.getStyleClass().add("options-pane"); + SplitPane textOutputs = new SplitPane(htmlText, cssText); + textOutputWrapper.setTop(options); + textOutputWrapper.setCenter(textOutputs); + textOutputs.setOrientation(Orientation.VERTICAL); + SplitPane split = new SplitPane(htmlView, textOutputWrapper); + setCenter(split); + heightProperty().addListener((ob, old, current) -> + // Odd, but the HTML view height doesn't sync, but the width does. + // So we need to manually apply resizing here for height changes. + htmlView.resize(htmlView.getWidth(), current.doubleValue())); + htmlView.setOnScroll(e -> { + // Allow [control] + [mouse-wheel] to change web-view zoom level. + if (e.isControlDown()) { + double zoom = htmlView.getZoom(); + if (zoom < 1 / 5.0 || zoom > 5) return; + + double speed = 1.125; + if (e.getDeltaY() > 0) htmlView.setZoom(zoom * speed); + else htmlView.setZoom(zoom / speed); + } + }); + // Property listeners + fullHtml.addListener((ob, old, current) -> { + if (!isHtmlGenerationInProgress && config != null) updateHtml(); + }); + includeJs.addListener((ob, old, current) -> { + if (!isHtmlGenerationInProgress && config != null) updateHtml(); + }); + inlineCss.addListener((ob, old, current) -> { + if (!isHtmlGenerationInProgress && config != null) updateHtml(); + }); + // Allow user changes to CSS text to update HTML output. + cssText.textProperty().addListener((ob, old, current) -> { + if (!isCssGenerationInProgress && config != null) updateHtml(); + }); + } + + @Override + public void onConfigChanged(Configuration config) { + this.config = config; + } + + @Override + public void onTargetLanguageSet(Language language) { + targetLanguage = language; + updateOutput(); + } + + @Override + public void onInput(String source) { + lastSource = source; + updateOutput(); + } + + private void updateOutput() { + updateCss(); + updateHtml(); + } + + private void updateCss() { + synchronized (cssText) { + isCssGenerationInProgress = true; + String css = createCss(); + cssText.setText(css); + isCssGenerationInProgress = false; + } + } + + private void updateHtml() { + if (lastBuildRequest == null || lastBuildRequest.isDone()) { + int scroll = WebScrollHelper.getVScrollValue(htmlView); + lastBuildRequest = executorService.submit(() -> { + HtmlGenResults html = createHtml(); + Platform.runLater(() -> { + isHtmlGenerationInProgress = true; + htmlText.setText(html.forText); + htmlView.getEngine().loadContent(html.forView); + htmlView.getEngine().documentProperty().addListener((ob, old, current) -> { + if (includeJs.get()) htmlView.getEngine().executeScript("setup()"); + // When the document is ready, restore prior scroll position + WebScrollHelper.scrollTo(htmlView, 0, scroll); + }); + isHtmlGenerationInProgress = false; + }); + }); + } else { + lastBuildRequest.cancel(true); + } + } + + private String createCss() { + // Get input values + Language language = targetLanguage; + try { + if (language == null) language = config.getLanguages().get(0); + } catch (Exception ex) { + return "/* No input language set! */"; + } + + return new LanguageMatcher(language).createPatternCSS(); + } + + private HtmlGenResults createHtml() { + // Validate source set + if (lastSource == null) { + return new HtmlGenResults("" + + "

Error: Missing inputs

" + + "

Source not set!

" + + ""); + } + + // Get input values + Language language = targetLanguage; + try { + if (language == null) language = config.getLanguages().get(0); + } catch (Exception ex) { + return new HtmlGenResults("" + + "

Error: Missing inputs

" + + "

Language not set!

" + + ""); + } + + // Convert + try { + // Because the scroll-bar can disappear from the web-view (which can look confusing) + // we're going to insert padding so that it is clear that when this occurs the UI isn't + // busted. The bar just is gone. + // + // Additionally, a horizontal overflow in the webview's contents (
) will make the
+			// last line not visible. So for our preview we're going to just disable it.
+			String viewFixes = "body { padding: 7px; }\n" +
+					"pre { overflow: hidden !IMPORTANT; }";
+			String css = BASE_CSS + "\n" + cssText.getText();
+
+			// Get 
 block
+			String codeBlock = new LanguageMatcher(language)
+					.convert(lastSource, inlineCss.get());
+
+			// Wrap in HTML body
+			StringBuilder sbWeb = new StringBuilder();
+
+			sbWeb.append("\n\n");
+			if (!inlineCss.get()) sbWeb.append("\n\n");
+			if (includeJs.get()) sbWeb.append("\n\n");
+			sbWeb.append("\n\n");
+			sbWeb.append("\n\n\n");
+			sbWeb.append(codeBlock);
+			sbWeb.append("\n\n\n");
+
+			if (fullHtml.get()) {
+				return new HtmlGenResults(sbWeb.toString());
+			} else {
+				return new HtmlGenResults(sbWeb.toString(), codeBlock);
+			}
+		} catch (Exception ex) {
+			StringWriter sw = new StringWriter();
+			ex.printStackTrace(new PrintWriter(sw));
+			return new HtmlGenResults("" +
+					"

Error: Conversion

" + + "

Exception occurred converting source:

" + + "
" +
+					sw +
+					"
" + + ""); + } + } + + /** + * Wrapper, since we want results to be different for the displayed content and the text for the user to copy. + */ + static class HtmlGenResults { + private final String forView; + private final String forText; + + public HtmlGenResults(String single) { + this(single, single); + } + + public HtmlGenResults(String forView, String forText) { + this.forView = forView; + this.forText = forText; + } + } +} diff --git a/src/main/java/me/coley/c2h/util/LanguageMatcher.java b/src/main/java/me/coley/c2h/util/LanguageMatcher.java new file mode 100644 index 0000000..f683408 --- /dev/null +++ b/src/main/java/me/coley/c2h/util/LanguageMatcher.java @@ -0,0 +1,331 @@ +package me.coley.c2h.util; + +import com.florianingerl.util.regex.Matcher; +import com.florianingerl.util.regex.Pattern; +import me.coley.c2h.config.model.Language; +import me.coley.c2h.config.model.Rule; +import me.coley.c2h.ui.pane.OutputPane; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +import static org.apache.commons.text.StringEscapeUtils.escapeHtml4; + +/** + * Tool that takes in some text, breaks it down based on matched regions from a {@link Language#getRules()} + * and any {@link Rule#getSubrules()}, then takes the broken down regions and builds HTML text from them. + * + * @author Matt Coley + */ +public class LanguageMatcher { + private static final Logger logger = LoggerFactory.getLogger(LanguageMatcher.class); + private static final Map, Pattern> patternCache = new HashMap<>(); + private final Language language; + + /** + * @param language + * Language to match against. + */ + public LanguageMatcher(Language language) { + if (language == null) { + throw new IllegalStateException("Language cannot be null"); + } + this.language = language; + } + + /** + * @param text + * Text to convert into HTML. + * @param inline + * Flag to enable inline CSS generation. + * + * @return HTML of the text with matched attributes of the {@link #language current language}. + */ + public String convert(String text, boolean inline) { + long start = System.currentTimeMillis(); + logger.trace("Running language to HTML conversion for {}", language.getName()); + Region root = new Region(text); + logger.trace("Splitting input text into blocks"); + root.split(language.getRules()); + logger.trace("Collecting blocks input HTML text"); + String html = root.toHtml(inline); + logger.trace("Completed conversion in {}ms", (System.currentTimeMillis() - start)); + return html; + } + + /** + * @return CSS for language. + */ + public String createPatternCSS() { + return "/* =========================== */\n" + + "/* Custom element types */\n" + + "/* =========================== */\n" + + createPatternCSS("", language.getRules()); + } + + /** + * @param parent + * Parent class name / context. + * @param rules + * Rules to create CSS classes for. + * + * @return CSS for given rule list. + */ + private static String createPatternCSS(String parent, List rules) { + StringBuilder sb = new StringBuilder(); + for (Rule rule : rules) { + String cssName = "." + rule.getName(); + sb.append(parent).append(cssName).append(" {"); + rule.getStyle().forEach(style -> + sb.append("\n\t").append(style.getKey()).append(": ") + .append(style.getValue()).append(";")); + sb.append("\n}\n"); + // Add sub-rules + if (!rule.getSubrules().isEmpty()) sb.append(createPatternCSS(cssName + " ", rule.getSubrules())); + } + return sb.toString(); + } + + private static class Region { + private final List children = new ArrayList<>(); + private final String text; + private final Rule rule; + private final int start; + private final int end; + + /** + * Constructor for root region. + * + * @param text + * Complete text. + */ + private Region(String text) { + this.text = text; + rule = null; + start = 0; + end = text.length(); + } + + /** + * Constructor for matching a rule. + * + * @param text + * Complete text. + * @param rule + * Rule associated with region. + * @param start + * Start offset in complete text. + * @param end + * End offset in complete text. + */ + private Region(String text, Rule rule, int start, int end) { + this.text = text; + this.rule = rule; + this.start = start; + this.end = end; + } + + /** + * Splits this node into subregions based on the given rules. + * + * @param rules + * Rules to match and split by. + */ + public void split(List rules) { + // Match within give region + String localText = text.substring(start, end); + Matcher matcher = getCombinedPattern(rules).matcher(localText); + while (matcher.find()) { + // Create region from found match + int localStart = matcher.start(); + int localEnd = matcher.end(); + Rule matchedRule = getRuleFromMatcher(rules, matcher); + logger.trace("Block range found [{} - {}] for rule '{}'", localStart, localEnd, matchedRule.getName()); + Region localChild = new Region(text, matchedRule, start + localStart, start + localEnd); + + // Break the new region into smaller ones if the rule associated with the match has sub-rules. + List subrules = matchedRule.getSubrules(); + if (!subrules.isEmpty()) localChild.split(subrules); + + // Add child (splitting technically handled in HTML output logic) + children.add(localChild); + } + } + + /** + * @param inline + * {@code true} to emit CSS inline with created {@code } elements. + * + * @return Styled HTML by spans matching the {@link Language#getRules() language rules} break-down. + */ + public String toHtml(boolean inline) { + StringBuilder sb = new StringBuilder(); + int lastEnd = start; + if (rule == null) { + logger.trace("Writing root region to HTML"); + + // Add children + for (Region child : children) { + int childStart = child.start; + + // Append text not matched at start + if (childStart > lastEnd) { + String unmatched = escapeHtml4(text.substring(lastEnd, childStart)); + sb.append(unmatched); + } + + // Append child content + sb.append(child.toHtml(inline)); + lastEnd = child.end; + } + // Append remaining unmatched text, then end preformatted block + sb.append(escapeHtml4(text.substring(lastEnd))); + + // Wrap in root
+				StringBuilder fmt = new StringBuilder();
+				if (inline) {
+					StringBuilder sbLineStyle = new StringBuilder();
+					StringBuilder sbLinePreStyle = new StringBuilder();
+					Regex.getCssProperties(OutputPane.BASE_CSS, "pre .line").forEach((key, value) ->
+							sbLineStyle.append(key).append(":").append(value).append(";"));
+					Regex.getCssProperties(OutputPane.BASE_CSS, "pre .line::before").forEach((key, value) ->
+							sbLinePreStyle.append(key).append(":").append(value).append(";"));
+					int lineNum = 1;
+					for (String line : sb.toString().split("\n")) {
+						fmt.append("").append(lineNum++)
+								.append("").append(line).append("\n");
+					}
+
+					// Wrap in pre tags and slap it in an HTML page
+					StringBuilder sbPreStyle = new StringBuilder();
+					Regex.getCssProperties(OutputPane.BASE_CSS, "pre").forEach((key, value) ->
+							sbPreStyle.append(key).append(":").append(value).append(";"));
+					return "
" + fmt + "
"; + } else { + fmt.append("
");
+					for (String line : sb.toString().split("\n")) {
+						fmt.append("").append(line).append("\n");
+					}
+					return fmt.append("
").toString(); + } + } else { + logger.trace("Writing region[{} - {}] to HTML", start, end); + if (inline) { + String styleRules = getInlineStyleFromRule(rule); + sb.append(""); + } else { + String styleClass = rule.getName(); + sb.append(""); + } + + // Add children + for (Region child : children) { + int childStart = child.start; + + // Append text not matched at start + if (childStart > lastEnd) { + String unmatched = escapeHtml4(text.substring(lastEnd, childStart)); + sb.append(unmatched); + } + + // Append child content + sb.append(child.toHtml(inline)); + lastEnd = child.end; + } + // Append remaining unmatched text, then end span block + sb.append(escapeHtml4(text.substring(lastEnd, end))); + sb.append(""); + } + return sb.toString(); + } + + @Override + public String toString() { + if (rule == null) return "ROOT"; + return rule.getName() + " {\n" + + text.substring(start, end) + + "\n}"; + } + + /** + * @param rules + * Rules to search for. + * @param matcher + * Matcher to pull rule name from using matched group's name. + * + * @return Rule with matching name as matched group. + */ + private static Rule getRuleFromMatcher(Collection rules, Matcher matcher) { + return rules.stream() + .filter(rule -> matcher.group(rule.getPatternGroupName()) != null) + .findFirst() + .orElse(null); + } + + /** + * @param rule + * Rule to pull style from. + * + * @return Inline CSS for the rule. + */ + private static String getInlineStyleFromRule(Rule rule) { + if (rule != null) { + StringBuilder sbStyle = new StringBuilder(); + rule.getStyle().forEach(style -> + sbStyle.append(style.getKey()).append(":").append(style.getValue()).append(";")); + return sbStyle.toString(); + } + return ""; + } + + /** + * @param rules + * Rules to match against. + * + * @return Compiled regex pattern from the given rules. + */ + private static Pattern getCombinedPattern(List rules) { + // Cache results. Very likely to encounter rule-collection again which makes it wise to save + // the result instead of re-computing every time. + return patternCache.computeIfAbsent(rules, Region::createCombinedPattern); + } + + /** + * @param rules + * Rules to match against. + * + * @return Compiled regex pattern from the given rules. + */ + private static Pattern createCombinedPattern(List rules) { + // Dummy pattern + if (rules.isEmpty()) return Regex.patternOf("(?EMPTY)"); + + // (?N) and (\N) in regex when wrapped in a group increment to ((?N+1)) and ((\N+1)) + // Since these are all bundled in named group our start offset is going to be 1. + // Any group that isn't non-matching '(?:foo)' should also increment the offset. + int relativeGroup = 1; + StringBuilder sb = new StringBuilder(); + logger.trace("Creating pattern for {} rules", rules.size()); + for (Rule rule : rules) { + logger.trace(" - Appending: " + rule.getName()); + + // Update (?N) and (\N) values since we are merging patterns into a single combined one + // - ?N - Subroutine matches expression defined in Nth capture group + // - \N - Back-reference matches same text matched by Nth capture group + String pattern = Regex.incrementRecursiveConstructs(rule.getPattern(), relativeGroup); + logger.trace(" - Incremented recursions: " + rule.getName()); + + // Append to builder + sb.append("(?<").append(rule.getPatternGroupName()).append(">").append(pattern).append(")|"); + + // Increment count + int localGroups = Regex.countMatches(pattern, "\\((?!\\?:)[^)(]*+(?:[^)(])*+\\)"); + logger.trace(" - Counted matches: " + rule.getName()); + relativeGroup += 1 + localGroups; + } + return Regex.patternOf(sb.substring(0, sb.length() - 1)); + } + } +} diff --git a/src/main/java/me/coley/c2h/util/Regex.java b/src/main/java/me/coley/c2h/util/Regex.java index a494691..19972d1 100644 --- a/src/main/java/me/coley/c2h/util/Regex.java +++ b/src/main/java/me/coley/c2h/util/Regex.java @@ -1,8 +1,7 @@ package me.coley.c2h.util; -import jregex.Matcher; -import jregex.Pattern; -import org.apache.commons.text.StringEscapeUtils; +import com.florianingerl.util.regex.Matcher; +import com.florianingerl.util.regex.Pattern; import java.util.HashMap; import java.util.Map; @@ -10,9 +9,11 @@ /** * Utility for miscellaneous regex actions. * - * @author Matt + * @author Matt Coley */ public class Regex { + private static final Map patternCache = new HashMap<>(); + /** * @param cssText * Text containing CSS to match. @@ -23,21 +24,99 @@ public class Regex { */ public static Map getCssProperties(String cssText, String cssRule) { Map map = new HashMap<>(); - String regex = "(({TITLE}" + cssRule + ")\\s*\\{({BODY}[^}]*))\\}"; - Pattern pattern = new Pattern(regex); + String regex = "((?" + cssRule + ")\\s*\\{(?<BODY>[^}]*))\\}"; + Pattern pattern = patternOf(regex); Matcher matcher = pattern.matcher(cssText); - if(!matcher.find()) + if (!matcher.find()) { return map; + } String body = matcher.group("BODY"); + // Parse the body of the css class and update the regex-rule style map. - regex = "({key}\\S+):\\s*({value}.+)(?=;)"; - pattern = new Pattern(regex); + regex = "(?<key>\\S+):\\s*(?<value>.+)(?=;)"; + pattern = patternOf(regex); matcher = pattern.matcher(body); - while(matcher.find()) { + while (matcher.find()) { String key = matcher.group("key"); String value = matcher.group("value"); map.put(key, value); } return map; } + + /** + * @param text + * Input text to search in. + * @param regex + * Pattern to search with. + * + * @return The number of matches in the input text. + */ + public static int countMatches(String text, String regex) { + int count = 0; + Matcher matcher = patternOf(regex).matcher(text); + while (matcher.find()) { + count++; + } + return count; + } + + /** + * @param regex + * Regex to update + * @param increment + * Value to increment with. + * + * @return Updated regex with new recursive constructs / back-references. + */ + public static String incrementRecursiveConstructs(String regex, int increment) { + Matcher matcher = patternOf("\\([?\\\\]\\d\\)").matcher(regex); + while (matcher.find()) { + int value = extractInt(matcher.group(0)); + regex = regex + .replace("(?" + value + ")", "(?" + (value + increment) + ")") + .replace("(\\" + value + ")", "(\\" + (value + increment) + ")"); + } + return regex; + } + + /** + * Only used in {@link #incrementRecursiveConstructs(String, int)} so input is assumed to be {@code (.N.)} where + * N is one or more consecutive numeric characters. Surrounding text discarded. + * + * @param text + * Input text containing a single integer. + * + * @return Int extracted. + */ + private static int extractInt(String text) { + StringBuilder sb = new StringBuilder(); + for (char c : text.toCharArray()) if (c >= '0' && c <= '9') sb.append(c); + return Integer.parseInt(sb.toString()); + } + + /** + * @param pattern + * Pattern text. + * + * @return Cached compiled pattern. + */ + public static Pattern patternOf(String pattern) { + return patternCache.computeIfAbsent(pattern, Pattern::compile); + } + + /** + * @param pattern + * Pattern text. + * + * @return {@code true} when valid. + */ + public static boolean isValid(String pattern) { + try { + Pattern.compile(pattern); + return true; + } catch (Throwable t) { + return false; + } + } } diff --git a/src/main/resources/code.js b/src/main/resources/code.js index 2fdcab0..1d6dd7c 100644 --- a/src/main/resources/code.js +++ b/src/main/resources/code.js @@ -1,7 +1,4 @@ -/** - * When the window loads, insert collapse buttons. - */ -window.onload = function() { +function setup() { // Iterate code blocks: var codeBlocks = document.getElementsByTagName("pre"); for (var i = 0; i < codeBlocks.length; i++) { @@ -121,4 +118,9 @@ function collapse(button, container, id) { */ function isEmpty(str) { return (!str || 0 === str.length); -} \ No newline at end of file +} + +// When the window loads, insert collapse buttons. +window.onload = function() { + setup(); +}; \ No newline at end of file diff --git a/src/main/resources/default-config.c2h b/src/main/resources/default-config.c2h deleted file mode 100644 index 71b79ca..0000000 --- a/src/main/resources/default-config.c2h +++ /dev/null @@ -1,84 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<configuration> - <language name="Java"> - <rule name="keywords"> - <pattern>\b(abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|native|new|package|private|protected|public|return|short|static|strictfp|super|switch|synchronized|this|throw|throws|transient|try|var|void|volatile|while)\b</pattern> - </rule> - <rule name="strings"> - <pattern>(\"(?:[^"\\]|\\.)*?\")|(\'(?:[^'\\]|\\.)*?\')</pattern> - </rule> - <rule name="constant"> - <pattern>(?:0[xX][0-9a-fA-F]+)+|(?:\b([\d._]*[\d])\b)+|\b(?:true|false|null)\b</pattern> - </rule> - <rule name="annotation"> - <pattern>\B(?:@[\w]+)\b</pattern> - </rule> - <rule name="single-line-comment"> - <pattern>//[^\n]+</pattern> - </rule> - <rule name="javadoc-comment"> - <pattern>/[*]{2}(?:.|\n)+?\*/</pattern> - </rule> - <rule name="multi-line-comment"> - <pattern>/[*](?:.|\n)+?\*/</pattern> - </rule> - <themes> - <theme name="clean"> - <style targetRule="keywords" key="color" value="rgb(127, 0, 85)" /> - <style targetRule="keywords" key="font-weight" value="bold" /> - <style targetRule="strings" key="color" value="rgb(47, 100, 31)" /> - <style targetRule="strings" key="font-weight" value="italic" /> - <style targetRule="constant" key="color" value="rgb(173, 53, 0)" /> - <style targetRule="annotation" key="color" value="rgb(120, 130, 150)" /> - <style targetRule="single-line-comment" key="color" value="rgb(0, 111, 12)" /> - <style targetRule="single-line-comment" key="font-style" value="italic" /> - <style targetRule="multi-line-comment" key="color" value="rgb(0, 111, 12)" /> - <style targetRule="multi-line-comment" key="font-style" value="italic" /> - <style targetRule="javadoc-comment" key="color" value="rgb(0, 100, 114)" /> - <style targetRule="javadoc-comment" key="font-style" value="italic" /> - </theme> - </themes> - </language> - <language name="Css"> - <rule name="comment"> - <pattern>/[*]+(.|\n)+?\*/</pattern> - </rule> - <rule name="attribute"> - <pattern>(?!\s+)[\w-]+(?=:)(?=.*;|.\s)</pattern> - </rule> - <rule name="string"> - <pattern>\"([^"\\]|\\.)*\"</pattern> - </rule> - <rule name="string-single"> - <pattern>\'([^'\\]|\\.)*\'</pattern> - </rule> - <rule name="at-rule"> - <pattern>@\w+</pattern> - </rule> - <rule name="rule"> - <pattern>[#.\-:\w]+(?=[\w\s\-:.,]*\{)</pattern> - </rule> - <rule name="numeric-constant"> - <pattern>[\d.]+px|[\d.]+%|[\d.]+em|[\d.]+deg|#\w+|\d+</pattern> - </rule> - <rule name="keywords"> - <pattern>\b(auto|rgb|rgba|transparent|visible|hidden|nowrap|left|right|center|justify|static|relative|fixed|absolute|sticky|block|inline|inline-block|none|initial|inherit|border-box|contain|repeat|no-repeat|repeat-x|repeat-y|italic|bold|oblique|dotted|dashed|solid|double|groove|ridge|inset|outset|none|hidden|black|silver|gray|white|maroon|red|purple|fuchsia|green|lime|olive|yellow|navy|blue|teal|aqua|antiquewhite|aquamarine|azure|beige|bisque|blanchedalmond|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|gainsboro|ghostwhite|gold|goldenrod|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|limegreen|linen|magenta|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|oldlace|olivedrab|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|thistle|tomato|turquoise|violet|wheat|whitesmoke|yellowgreen)\b</pattern> - </rule> - <themes> - <theme name="clean"> - <style targetRule="comment" key="color" value="rgb(0, 111, 12)" /> - <style targetRule="comment" key="font-style" value="italic" /> - <style targetRule="attribute" key="color" value="rgb(127, 0, 85)"/> - <style targetRule="string" key="color" value="rgb(47, 100, 31)" /> - <style targetRule="string" key="font-weight" value="italic" /> - <style targetRule="string-single" key="color" value="rgb(47, 100, 31)" /> - <style targetRule="string-single" key="font-weight" value="italic" /> - <style targetRule="numeric-constant" key="color" value="rgb(173, 53, 0)" /> - <style targetRule="keywords" key="color" value="rgb(173, 53, 0)" /> - <style targetRule="at-rule" key="color" value="rgb(59, 79, 119)" /> - <style targetRule="rule" key="color" value="rgb(0, 55, 100)" /> - <style targetRule="rule" key="font-weight" value="bold" /> - </theme> - </themes> - </language> -</configuration> diff --git a/src/main/resources/default-config.xml b/src/main/resources/default-config.xml new file mode 100644 index 0000000..0051ff9 --- /dev/null +++ b/src/main/resources/default-config.xml @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<configuration> + <language name="Java"> + <rule name="keywords"> + <pattern>\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|native|new|package|private|protected|public|return|short|static|strictfp|super|switch|synchronized|this|throw|throws|transient|try|var|void|volatile|while)\b</pattern> + <style> + <entry key="color" value="rgb(127, 0, 85)" /> + <entry key="font-weight" value="bold" /> + </style> + </rule> + <rule name="strings"> + <pattern>(?:\"(?:[^"\\]|\\.)*?\")|(?:\'(?:[^'\\]|\\.)*?\')</pattern> + <style> + <entry key="color" value="rgb(50, 100, 30)" /> + <entry key="font-weight" value="italic" /> + </style> + <subrules> + <rule name="escape"> + <pattern>\\.</pattern> + <style> + <entry key="color" value="rgb(150, 90, 40)" /> + </style> + </rule> + </subrules> + </rule> + <rule name="constant"> + <pattern>(?:0[xX][0-9a-fA-F]+)+|(?:\b(?:[\d._]*[\d])\b)+|\b(?:true|false|null)\b</pattern> + <style> + <entry key="color" value="rgb(170, 50, 0)" /> + </style> + </rule> + <rule name="annotation"> + <pattern>\B(?:@[\w]+)\b</pattern> + <style> + <entry key="color" value="rgb(120, 130, 150)" /> + </style> + </rule> + <rule name="todo-line-comment"> + <pattern>([ \t]+)(?:\/\/[^\n]*TODO\b[^\n]+\n)(\/\/[^\n]+)?\n?((\1)(?2)\n)*</pattern> + <style> + <entry key="color" value="rgb(50, 150, 0)" /> + <entry key="font-style" value="italic" /> + <entry key="font-weight" value="bold" /> + </style> + </rule> + <rule name="single-line-comment"> + <pattern>//[^\n]+</pattern> + <style> + <entry key="color" value="rgb(0, 110, 10)" /> + <entry key="font-style" value="italic" /> + </style> + </rule> + <rule name="javadoc-comment"> + <pattern>/[*]{2}(?:.|\n)+?\*/</pattern> + <style> + <entry key="color" value="rgb(0, 100, 120)" /> + <entry key="font-style" value="italic" /> + </style> + <subrules> + <rule name="link-reference"> + <pattern>(?<=\{\@link)\s+\S+?[\s\S]*?(?=})</pattern> + <style> + <entry key="color" value="rgb(60, 60, 20)" /> + </style> + </rule> + <rule name="field"> + <pattern>\B(?:@[\w]+)\b</pattern> + <style> + <entry key="font-weight" value="bold" /> + </style> + </rule> + <rule name="param-name"> + <pattern>(?<=@param)\s+\b(?:\w+)\b</pattern> + <style> + <entry key="color" value="rgb(0, 50, 60)" /> + </style> + </rule> + </subrules> + </rule> + <rule name="multi-line-comment"> + <pattern>/[*](?:.|\n)+?\*/</pattern> + <style> + <entry key="color" value="rgb(0, 110, 10)" /> + <entry key="font-style" value="italic" /> + </style> + </rule> + </language> + <language name="Css"> + <rule name="comment"> + <pattern>/[*]+(.|\n)+?\*/</pattern> + <style> + <entry key="color" value="rgb(0, 111, 10)" /> + <entry key="font-style" value="italic" /> + </style> + </rule> + <rule name="attribute"> + <pattern>(?!\s+)[\w-]+(?=:)(?=.*;|.\s)</pattern> + <style> + <entry key="color" value="rgb(127, 0, 85)" /> + </style> + </rule> + <rule name="string"> + <pattern>(\"(?:[^"\\]|\\.)*?\")|(\'(?:[^'\\]|\\.)*?\')</pattern> + <style> + <entry key="color" value="rgb(50, 100, 30)" /> + <entry key="font-weight" value="italic" /> + </style> + </rule> + <rule name="at-rule"> + <pattern>@\w+</pattern> + <style> + <entry key="color" value="rgb(60, 80, 120)" /> + </style> + </rule> + <rule name="rule"> + <pattern>[#.\-:\w]+(?=[\w\s\-:.,]*\{)</pattern> + <style> + <entry key="color" value="rgb(0, 55, 100)" /> + <entry key="font-weight" value="bold" /> + </style> + </rule> + <rule name="numeric-constant"> + <pattern>[\d.]+px|[\d.]+%|[\d.]+em|[\d.]+deg|#\w+|\d+</pattern> + <style> + <entry key="color" value="rgb(170, 50, 0)" /> + </style> + </rule> + <rule name="keywords"> + <pattern>\b(auto|rgb|rgba|transparent|visible|hidden|nowrap|left|right|center|justify|static|relative|fixed|absolute|sticky|block|inline|inline-block|none|initial|inherit|border-box|contain|repeat|no-repeat|repeat-x|repeat-y|italic|bold|oblique|dotted|dashed|solid|double|groove|ridge|inset|outset|none|hidden|black|silver|gray|white|maroon|red|purple|fuchsia|green|lime|olive|yellow|navy|blue|teal|aqua|antiquewhite|aquamarine|azure|beige|bisque|blanchedalmond|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|gainsboro|ghostwhite|gold|goldenrod|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|limegreen|linen|magenta|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|oldlace|olivedrab|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|thistle|tomato|turquoise|violet|wheat|whitesmoke|yellowgreen)\b</pattern> + <style> + <entry key="color" value="rgb(173, 53, 0)" /> + </style> + </rule> + </language> +</configuration> diff --git a/src/main/resources/gui.css b/src/main/resources/gui.css index 1c946b2..bd42cc6 100644 --- a/src/main/resources/gui.css +++ b/src/main/resources/gui.css @@ -1,24 +1,161 @@ -/* Labels have margins of about 13px */ -.tab-large { -fx-pref-width: 123px; } -.tab-large > .tab-container > .tab-label { - -fx-alignment: CENTER; - -fx-pref-width: 110px; - -fx-text-fill: -fx-text-base-color; -} -.tab-small { -fx-pref-width: 73px; } -.tab-small > .tab-container > .tab-label { - -fx-alignment: CENTER; - -fx-pref-width: 60px; - -fx-text-fill: -fx-text-base-color; -} -.title { - -fx-font-family: Arial; - -fx-font-weight: bold; +/* root, used to define colors */ +* { + -fx-base: rgb(255, 255, 255); + -fx-dark-text-color: rgb(0, 0, 0); + -fx-mid-text-color: rgb(0, 0, 0); + -fx-light-text-color: rgb(0, 0, 0); + -fx-accent-base: transparent; + -fx-accent-light: transparent; + -fx-accent-lighter: transparent; + -fx-accent: transparent; + -fx-focus-color: transparent; + -fx-bg: rgb(255, 255, 255); + -fx-bg-dark: rgb(230, 230, 230); + -fx-bg-darker: rgb(200, 200, 200); } -.pattern { - -fx-font-family: monospace; + +/* ========== Tabs ======================== */ +.tab-pane { + -fx-tab-min-width: 100px; + -fx-border-width: 0; +} +.tab-header-area { + -fx-padding: 0; + -fx-border-width: 0; +} +.tab-header-background { + -fx-background-color: -fx-bg-dark; + -fx-border-width: 0; +} +.tab { + -fx-background-color: -fx-bg-dark; + -fx-background-radius: 0; + -fx-background-insets: -1; + -fx-padding: 0px; + -fx-pref-width: 150px; + -fx-pref-height: 35px; +} +.tab:selected { + -fx-background-color: -fx-bg; + -fx-border-color: transparent !IMPORTANT; + -fx-border-width: 0; +} +.tab > .tab-container { + -fx-padding: 0px; +} +.tab > .tab-container > .tab-label { + -fx-text-fill: -fx-text-base-color; + -fx-pref-width: 150px; +} + +/* We don't want a focus indicator, we're using the border, and that's it */ +.tab:selected .focus-indicator { + -fx-border-color: transparent; } + +/* ========== Text ======================== */ +.text-field, +.text-area { + -fx-highlight-fill: rgba(150, 200, 255, 0.5); + -fx-highlight-text-fill: black; + -fx-border-width: 0px; + -fx-background-radius: 0px; + -fx-background-insets: 0; + -fx-background-color: -fx-bg; +} +.text-area .content { + -fx-background-color: transparent; + -fx-padding: 10px; +} +.input { + -fx-background-color: -fx-bg-dark; + -fx-border-color: -fx-bg-darker; + -fx-border-width: 1px; +} + +/* ========== Buttons ======================== */ +/* ========== Buttons ======================== */ +.check-box > .box, +.radio-button > .radio, .button { - -fx-pref-width: 120px; - -fx-padding: 5px; + -fx-border-width: 0px; + -fx-border-color: transparent; + -fx-border-radius: 2px; + -fx-background-radius: 2px; + -fx-background-color: -fx-bg-dark; +} +.check-box:hover > .box, +.radio-button:hover > .radio, +.button:hover { + -fx-background-color: -fx-bg-darker; +} +.check-box > .box, +.radio-button > .radio { + -fx-background-color: -fx-bg-dark; +} +.radio-button > .radio { + -fx-background-radius: 20px; + -fx-border-radius: 20px; +} +.button:pressed { + -fx-background-color: -fx-bg-darker; +} + +/* ========== Scrollbar ======================== */ +.scroll-bar .track { + -fx-background-color: -fx-bg; + -fx-background-radius: 0; +} +.scroll-bar .thumb { + -fx-background-color: -fx-bg-darker; + -fx-background-radius: 0; +} +.scroll-bar .thumb:hover, +.scroll-bar .thumb:pressed { + -fx-background-color: -fx-bg-dark; +} +.scroll-bar .increment-button, +.scroll-bar .decrement-button { + -fx-background-color: transparent; + -fx-border-width: 0; +} +.scroll-bar .increment-button:hover, +.scroll-bar .decrement-button:hover { + -fx-background-color: transparent; + -fx-border-width: 0; +} +.scroll-bar .increment-button .increment-arrow, +.scroll-bar .decrement-button .decrement-arrow, +.scroll-bar .increment-button:hover .increment-arrow, +.scroll-bar .decrement-button:hover .decrement-arrow { + -fx-background-color: -fx-bg-darker; +} + +/* ========== Layouts ======================== */ +.split-pane { + -fx-padding: 0px; + -fx-box-border: transparent; +} +.split-pane:horizontal > .split-pane-divider { + -fx-background-color: -fx-bg-dark; +} +.split-pane:vertical > .split-pane-divider { + -fx-background-color: -fx-bg-dark; +} +.options-pane { + -fx-padding: 5px; +} + +/* ========== Misc ======================== */ +.h1 { + -fx-font-size: 24px; +} +.h2 { + -fx-font-size: 17px; +} +.pattern { + -fx-font-family: monospace; +} +.mono { + -fx-font-family: monospace; } \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..f45ed87 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,10 @@ +<configuration> + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level: %msg%n</pattern> + </encoder> + </appender> + <root level="trace"> + <appender-ref ref="debug"/> + </root> +</configuration> \ No newline at end of file diff --git a/src/test/java/me/coley/c2h/loader/ExporterTest.java b/src/test/java/me/coley/c2h/loader/ExporterTest.java index 0c70b5b..8434da2 100644 --- a/src/test/java/me/coley/c2h/loader/ExporterTest.java +++ b/src/test/java/me/coley/c2h/loader/ExporterTest.java @@ -3,40 +3,52 @@ import me.coley.c2h.config.Exporter; import me.coley.c2h.config.Importer; import me.coley.c2h.config.model.Configuration; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import javax.xml.bind.JAXBException; import java.io.IOException; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; -public class ExporterTest { - private Configuration configuration; +@Disabled +public class ExporterTest +{ + private Configuration configuration; - @Before - public void setup() { - try { - // Its bad practice to require disk resources in tests, but until somebody gets around - // to mocking a model configuration, its good enough. - // If it fails, good to catch it here anyways. - configuration = Importer.importDefault(); - } catch(JAXBException e) { - fail("setup: JAXBException" + e.getMessage()); - } catch(IOException e) { - fail("setup: IOException" + e.getMessage()); - } - } + @BeforeEach + public void setup() + { + try + { + // Its bad practice to require disk resources in tests, but until somebody gets around + // to mocking a model configuration, its good enough. + // If it fails, good to catch it here anyways. + configuration = Importer.importDefault(); + } catch (JAXBException e) + { + fail("setup: JAXBException" + e.getMessage()); + } catch (IOException e) + { + fail("setup: IOException" + e.getMessage()); + } + } - @Test - public void testExport() { - try { - String confStr = Exporter.toString(configuration); - assertNotNull(confStr); - Configuration copy = Importer.importFromText(confStr); - assertArrayEquals(configuration.getLanguages().toArray(), copy.getLanguages().toArray()); - } catch(JAXBException e) { - fail("testExport: JAXBException" + e.getMessage()); - } - } + @Test + public void testExport() + { + try + { + String confStr = Exporter.toXML(configuration); + assertNotNull(confStr); + Configuration copy = Importer.importFromText(confStr); + assertArrayEquals(configuration.getLanguages().toArray(), copy.getLanguages().toArray()); + } catch (JAXBException e) + { + fail("testExport: JAXBException" + e.getMessage()); + } + } } \ No newline at end of file diff --git a/src/test/java/me/coley/c2h/loader/ImporterTest.java b/src/test/java/me/coley/c2h/loader/ImporterTest.java index 30edfc4..f62526c 100644 --- a/src/test/java/me/coley/c2h/loader/ImporterTest.java +++ b/src/test/java/me/coley/c2h/loader/ImporterTest.java @@ -1,44 +1,53 @@ package me.coley.c2h.loader; import me.coley.c2h.config.Importer; -import me.coley.c2h.config.model.*; -import org.junit.Before; -import org.junit.Test; +import me.coley.c2h.config.model.Configuration; +import me.coley.c2h.config.model.Language; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import javax.xml.bind.JAXBException; import java.io.IOException; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; -public class ImporterTest { - private Configuration configuration; +@Disabled +public class ImporterTest +{ + private Configuration configuration; - @Before - public void setup() { - try { - // Its bad practice to require disk resources in tests, but until somebody gets around - // to mocking a model configuration, its good enough. - // If it fails, good to catch it here anyways. - configuration = Importer.importDefault(); - } catch(JAXBException e) { - fail("setup: JAXBException" + e.getMessage()); - } catch(IOException e) { - fail("setup: IOException" + e.getMessage()); - } - } + @BeforeEach + public void setup() + { + try + { + // Its bad practice to require disk resources in tests, but until somebody gets around + // to mocking a model configuration, its good enough. + // If it fails, good to catch it here anyways. + configuration = Importer.importDefault(); + } catch (JAXBException e) + { + fail("setup: JAXBException" + e.getMessage()); + } catch (IOException e) + { + fail("setup: IOException" + e.getMessage()); + } + } - @Test - public void testNonEmpty() { - for(Language lang : configuration.getLanguages()) { - // No nulls allows in language models - assertNotNull(lang.getName()); - assertNotNull(lang.getRules()); - assertNotNull(lang.getThemes()); - // Some content exists - assertTrue(!lang.getRules().isEmpty()); - assertTrue(!lang.getThemes().isEmpty()); - } - } + @Test + public void testNonEmpty() + { + for (Language lang : configuration.getLanguages()) + { + // No nulls allows in language models + assertNotNull(lang.getName()); + assertNotNull(lang.getRules()); + // Some content exists + assertFalse(lang.getRules().isEmpty()); + } + } } \ No newline at end of file diff --git a/src/test/java/me/coley/c2h/regex/ConfigHelperTest.java b/src/test/java/me/coley/c2h/regex/ConfigHelperTest.java deleted file mode 100644 index 61c4bb5..0000000 --- a/src/test/java/me/coley/c2h/regex/ConfigHelperTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package me.coley.c2h.regex; - -import me.coley.c2h.config.ConfigHelper; -import me.coley.c2h.config.Importer; -import me.coley.c2h.config.model.*; -import org.junit.Before; -import org.junit.Test; - -import javax.xml.bind.JAXBException; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -public class ConfigHelperTest { - private ConfigHelper helper; - - @Before - public void setup() throws JAXBException { - Configuration configuration = Importer.importFromText("<configuration><language " + - "name=\"test\"><themes><theme " + - "name=\"test\"></theme></themes></language></configuration>"); - Language language = configuration.getLanguages().get(0); - Theme theme = language.getThemes().get(0); - helper = new ConfigHelper(configuration, language, theme); - } - - @Test - public void testSingleRule() { - helper.addRule("WORD", "\\w+"); - assertEquals("({WORD}\\w+)", getPat()); - } - - @Test - public void testTwoRules() { - helper.addRule("COLOR", "(red)|(green)|(blue)"); - helper.addRule("NUMBR", "(one)|(two)|(three)"); - assertEquals("({COLOR}(red)|(green)|(blue))|({NUMBR}(one)|(two)|(three))", getPat()); - } - - @Test - public void testSingleStyle() { - helper.addRule("WORD", "\\w+"); - helper.getTheme().update("WORD", "color", "red"); - // - String expected = ".WORD {\n\tcolor: red;\n}"; - assertTrue(helper.getPatternCSS().contains(expected)); - } - - @Test - public void testTwoStyles() { - helper.addRule("WORD", "\\w+"); - helper.addRule("NUMBR", "\\d+"); - helper.getTheme().update("WORD", "color", "red"); - helper.getTheme().update("NUMBR", "color", "blue"); - // - String expected = ".WORD {\n\tcolor: red;\n}\n.NUMBR {\n\tcolor: blue;\n}"; - assertTrue(helper.getPatternCSS().contains(expected)); - } - - private String getPat() { - return helper.getPattern().toString(); - } -} \ No newline at end of file