Skip to content

Commit

Permalink
Nano SystemHighlighter: add theme system (#752)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattirn authored Nov 21, 2021
1 parent 4010953 commit 5cef3ba
Show file tree
Hide file tree
Showing 11 changed files with 271 additions and 111 deletions.
155 changes: 121 additions & 34 deletions builtins/src/main/java/org/jline/builtins/Nano.java
Original file line number Diff line number Diff line change
Expand Up @@ -1426,17 +1426,31 @@ protected static SyntaxHighlighter build(List<Path> syntaxFiles, String file, St
, boolean ignoreErrors) {
SyntaxHighlighter out = new SyntaxHighlighter();
List<HighlightRule> defaultRules = new ArrayList<>();
Map<String, String> colorTheme = new HashMap<>();
try {
if (syntaxName == null || (syntaxName != null && !syntaxName.equals("none"))) {
for (Path p : syntaxFiles) {
try {
NanorcParser parser = new NanorcParser(p, syntaxName, file);
parser.parse();
if (parser.matches()) {
out.addRules(parser.getHighlightRules());
return out;
} else if (parser.isDefault()) {
defaultRules.addAll(parser.getHighlightRules());
if (colorTheme.isEmpty() && p.getFileName().toString().endsWith(".nanorctheme")) {
try (BufferedReader reader = new BufferedReader(new FileReader(p.toFile()))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.length() > 0 && !line.startsWith("#")) {
List<String> parts = Arrays.asList(line.split("\\s+", 2));
colorTheme.put(parts.get(0), parts.get(1));
}
}
}
} else {
NanorcParser parser = new NanorcParser(p, syntaxName, file, colorTheme);
parser.parse();
if (parser.matches()) {
out.addRules(parser.getHighlightRules());
return out;
} else if (parser.isDefault()) {
defaultRules.addAll(parser.getHighlightRules());
}
}
} catch (IOException e) {
// ignore
Expand Down Expand Up @@ -1464,8 +1478,8 @@ public static SyntaxHighlighter build(Path nanorc, String syntaxName) {
List<Path> syntaxFiles = new ArrayList<>();
try {
try (BufferedReader reader = new BufferedReader(new FileReader(nanorc.toFile()))) {
String line = reader.readLine();
while (line != null) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.length() > 0 && !line.startsWith("#")) {
List<String> parts = Parser.split(line);
Expand All @@ -1481,9 +1495,19 @@ public static SyntaxHighlighter build(Path nanorc, String syntaxName) {
} else {
syntaxFiles.add(Paths.get(parts.get(1)));
}
} else if(parts.get(0).equals("theme")) {
if (parts.get(1).contains("*") || parts.get(1).contains("?")) {
PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + parts.get(1));
Optional<Path> theme = Files.find(Paths.get(new File(parts.get(1)).getParent()), Integer.MAX_VALUE, (path, f) -> pathMatcher.matches(path))
.findFirst();
if (theme.isPresent()) {
syntaxFiles.add(0, theme.get());
}
} else {
syntaxFiles.add(0, Paths.get(parts.get(1)));
}
}
}
line = reader.readLine();
}
}
out = build(syntaxFiles, null, syntaxName);
Expand Down Expand Up @@ -1678,11 +1702,13 @@ private static class NanorcParser {
private final String target;
private final List<HighlightRule> highlightRules = new ArrayList<>();
private final BufferedReader reader;
private Map<String, String> colorTheme = new HashMap<>();
private boolean matches = false;
private String syntaxName = "unknown";

public NanorcParser(Path file, String name, String target) throws IOException {
public NanorcParser(Path file, String name, String target, Map<String, String> colorTheme) throws IOException {
this(new Source.PathSource(file, null).read(), name, target);
this.colorTheme = colorTheme;
}

public NanorcParser(InputStream in, String name, String target) {
Expand All @@ -1698,21 +1724,7 @@ public void parse() throws IOException {
idx++;
line = line.trim();
if (line.length() > 0 && !line.startsWith("#")) {
line = line.replaceAll("\\\\<", "\\\\b")
.replaceAll("\\\\>", "\\\\b")
.replaceAll("\\[:alnum:]", "\\\\p{Alnum}")
.replaceAll("\\[:alpha:]", "\\\\p{Alpha}")
.replaceAll("\\[:blank:]", "\\\\p{Blank}")
.replaceAll("\\[:cntrl:]", "\\\\p{Cntrl}")
.replaceAll("\\[:digit:]", "\\\\p{Digit}")
.replaceAll("\\[:graph:]", "\\\\p{Graph}")
.replaceAll("\\[:lower:]", "\\\\p{Lower}")
.replaceAll("\\[:print:]", "\\\\p{Print}")
.replaceAll("\\[:punct:]", "\\\\p{Punct}")
.replaceAll("\\[:space:]", "\\\\s")
.replaceAll("\\[:upper:]", "\\\\p{Upper}")
.replaceAll("\\[:xdigit:]", "\\\\p{XDigit}");
List<String> parts = Parser.split(line);
List<String> parts = Parser.split(fixRegexes(line));
if (parts.get(0).equals("syntax")) {
syntaxName = parts.get(1);
List<Pattern> filePatterns = new ArrayList<>();
Expand All @@ -1726,7 +1738,7 @@ public void parse() throws IOException {
for (int i = 2; i < parts.size(); i++) {
filePatterns.add(Pattern.compile(parts.get(i)));
}
for (Pattern p: filePatterns) {
for (Pattern p : filePatterns) {
if (p.matcher(target).find()) {
matches = true;
break;
Expand All @@ -1738,16 +1750,81 @@ public void parse() throws IOException {
} else {
matches = true;
}
} else if (parts.get(0).equals("color")) {
addHighlightRule(syntaxName + idx, parts, false);
} else if (parts.get(0).equals("icolor")) {
addHighlightRule(syntaxName + idx, parts, true);
} else if (!addHighlightRule(parts, idx) && parts.get(0).matches("\\+[A-Z_]+")) {
String key = themeKey(parts.get(0));
if (colorTheme.containsKey(key)) {
for (String l : colorTheme.get(key).split("\\\\n")) {
idx++;
addHighlightRule(Parser.split(fixRegexes(l)), idx);
}
} else {
Log.warn("Unknown token type: ", key);
}
}
}
}
reader.close();
}

private String fixRegexes(String line) {
return line.replaceAll("\\\\<", "\\\\b")
.replaceAll("\\\\>", "\\\\b")
.replaceAll("\\[:alnum:]", "\\\\p{Alnum}")
.replaceAll("\\[:alpha:]", "\\\\p{Alpha}")
.replaceAll("\\[:blank:]", "\\\\p{Blank}")
.replaceAll("\\[:cntrl:]", "\\\\p{Cntrl}")
.replaceAll("\\[:digit:]", "\\\\p{Digit}")
.replaceAll("\\[:graph:]", "\\\\p{Graph}")
.replaceAll("\\[:lower:]", "\\\\p{Lower}")
.replaceAll("\\[:print:]", "\\\\p{Print}")
.replaceAll("\\[:punct:]", "\\\\p{Punct}")
.replaceAll("\\[:space:]", "\\\\s")
.replaceAll("\\[:upper:]", "\\\\p{Upper}")
.replaceAll("\\[:xdigit:]", "\\\\p{XDigit}");
}

private boolean addHighlightRule(List<String> parts, int idx) {
boolean out = true;
if (parts.get(0).equals("color")) {
addHighlightRule(syntaxName + idx, parts, false);
} else if (parts.get(0).equals("icolor")) {
addHighlightRule(syntaxName + idx, parts, true);
} else if (parts.get(0).matches("[A-Z_]+[:]?")) {
String key = themeKey(parts.get(0));
if (colorTheme.containsKey(key)) {
parts.set(0, "color");
parts.add(1, colorTheme.get(key));
addHighlightRule(syntaxName + idx, parts, false);
} else {
Log.warn("Unknown token type: ", key);
}
} else if (parts.get(0).matches("~[A-Z_]+[:]?")) {
String key = themeKey(parts.get(0));
if (colorTheme.containsKey(key)) {
parts.set(0, "icolor");
parts.add(1, colorTheme.get(key));
addHighlightRule(syntaxName + idx, parts, true);
} else {
Log.warn("Unknown token type: ", key);
}
} else {
out = false;
}
return out;
}

private String themeKey(String key) {
if (key.startsWith("+")) {
return key;
} else {
int keyEnd = key.endsWith(":") ? key.length() - 1 : key.length();
if (key.startsWith("~")) {
return key.substring(1, keyEnd);
}
return key.substring(0, keyEnd);
}
}

public boolean matches() {
return matches;
}
Expand Down Expand Up @@ -2062,8 +2139,8 @@ public Nano(Terminal terminal, Path root, Options opts, ConfigurationPath config

private void parseConfig(Path file) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(file.toFile()))) {
String line = reader.readLine();
while (line != null) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.length() > 0 && !line.startsWith("#")) {
List<String> parts = Parser.split(line);
Expand All @@ -2075,6 +2152,17 @@ private void parseConfig(Path file) throws IOException {
} else {
syntaxFiles.add(Paths.get(parts.get(1)));
}
} else if(parts.get(0).equals("theme")) {
if (parts.get(1).contains("*") || parts.get(1).contains("?")) {
PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + parts.get(1));
Optional<Path> theme = Files.find(Paths.get(new File(parts.get(1)).getParent()), Integer.MAX_VALUE, (path, f) -> pathMatcher.matches(path))
.findFirst();
if (theme.isPresent()) {
syntaxFiles.add(0, theme.get());
}
} else {
syntaxFiles.add(0, Paths.get(parts.get(1)));
}
} else if (parts.size() == 2
&& (parts.get(0).equals("set") || parts.get(0).equals("unset"))) {
String option = parts.get(1);
Expand Down Expand Up @@ -2136,7 +2224,6 @@ private void parseConfig(Path file) throws IOException {
errorMessage = "Nano config: Bad configuration '" + line + "'";
}
}
line = reader.readLine();
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion demo/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@
<resource>
<directory>src/main/scripts</directory>
<includes>
<include>*.nanorc</include>
<include>*.nanorc*</include>
</includes>
</resource>
</resources>
Expand Down
1 change: 1 addition & 0 deletions demo/src/main/java/org/jline/demo/Repl.java
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ public static void main(String[] args) {
File jnanorcFile = Paths.get(root, "jnanorc").toFile();
if (!jnanorcFile.exists()) {
try (FileWriter fw = new FileWriter(jnanorcFile)) {
fw.write("theme " + root + "nanorc/*.nanorctheme\n");
fw.write("include " + root + "nanorc/*.nanorc\n");
}
}
Expand Down
21 changes: 10 additions & 11 deletions demo/src/main/scripts/args.nanorc
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
syntax "ARGS"

color brightblue "\<[-]?[0-9]*([Ee][+-]?[0-9]+)?\>" "\<[-]?[0](\.[0-9]+)?\>"
color yellow ""(\\.|[^"])*"|'(\\.|[^'])*'|[a-zA-Z]+[a-zA-Z0-9]*"
color green "\<(console|grab|inspect)\>"
color cyan "\<null\>"
color brightcyan "\<(true|false)\>"
color brightyellow "\"(\\"|[^"])*\"\s*:" "'(\'|[^'])*'\s*:" "(\[|,)\s*[a-zA-Z0-9]*\s*:"
color white "(:|\[|,|\])"
color magenta "\\u[0-9a-fA-F]{4}|\\[bfnrt'"/\\]"
color blue start="/\*" end="\*/"
color blue "(//.*)"
color ,red " + +| + +"
NUMBER: "\<[-]?[0-9]*([Ee][+-]?[0-9]+)?\>" "\<[-]?[0](\.[0-9]+)?\>"
STRING: ""(\\.|[^"])*"|'(\\.|[^'])*'|[a-zA-Z]+[a-zA-Z0-9]*"
FUNCTION: "\<(console|grab|inspect)\>"
NULL: "\<null\>"
BOOLEAN: "\<(true|false)\>"
VARIABLE: "\"(\\"|[^"])*\"\s*:" "'(\'|[^'])*'\s*:" "(\[|,)\s*[a-zA-Z0-9]*\s*:"
PLAIN: "(:|\[|,|\])"
ESCAPE: "\\u[0-9a-fA-F]{4}|\\[bfnrt'"/\\]"
COMMENT: start="/\*" end="\*/"
COMMENT: "(//.*)"
10 changes: 5 additions & 5 deletions demo/src/main/scripts/command.nanorc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
syntax "COMMAND"

color green "[a-zA-Z]+[a-zA-Z0-9]*"
color yellow ".*="
color white "(\"|'|\.|=|:|\[|,|\])"
color blue start="/\*" end="\*/"
color blue "(^|[[:space:]])#.*$"
FUNCTION: "[a-zA-Z]+[a-zA-Z0-9]*"
VARIABLE: ".*="
PLAIN: "(\"|'|\.|=|:|\[|,|\])"
COMMENT: start="/\*" end="\*/"
COMMENT: "(^|[[:space:]])#.*$"
63 changes: 63 additions & 0 deletions demo/src/main/scripts/default.nanorctheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#
# This file describes a default scheme for nanorc syntax highlighting.
#
# Everything after a # character is a comment up to the end of the line.
# Comments are ignored. Empty lines are ignored too. Leading/trailing white
# space characters are removed before theme file is processed.
#
# Each line of the theme file describes a token type and how this token type
# should be colored (highlighted). The first word on each line is the name
# of the token type. After the name of the token type at least one white space
# character must follow, and then a text and background color
# for the highlighting must be specified, separated by a comma. No spaces
# are allowed inside color definition (that is, color definition is considered
# a single word, despite a possible comma).
#
# Background color can be omitted (in which case default background color
# of the terminal will be used). If you are omitting the background color,
# a comma may be omitted also. Likewise, a text color can be omitted,
# but comma must be present in this case.
#
# Author: Yuri Sakhno
# ysakhno at gmail dot com
#
# https://github.com/YSakhno/nanorc/
#

PLAIN white
FUNCTION brightgreen
STRING brightcyan
COMMENT cyan
DOC_COMMENT brightcyan
TYPE brightblue
BOOLEAN brightwhite
NULL cyan
NUMBER blue
VARIABLE brightyellow
PACKAGE green,,faint
CLASS green
CONSTANT yellow
OPERATOR yellow
OPTION yellow
KEYWORD brightwhite
MACRO brightmagenta
REGEXP blue,cyan
ESCAPE black,cyan
DELIMITER brightred
JUMP brightcyan
WARNING brightyellow,red
SECTION brightgreen
TAG brightwhite
ATTRIBUTE green
CHARREF brightred
PATH brightblue
URL brightblue
EMAIL brightblue
WHITESPACE ,green
#
# mixin
#
+FUNCTION FUNCTION: "[A-Za-z_][A-Za-z0-9_]*[[:space:]]*[(]" \n PLAIN: "[(]"
+TODO color brightwhite,cyan "FIXME|TODO|XXX"
+LINT color ,green "[[:space:]]+$" \n color ,red "\t*"
+LONG_LINE_WARNING color ,red "^.{81,}$"
16 changes: 7 additions & 9 deletions demo/src/main/scripts/gron.nanorc
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
syntax "GRON" "\.gron$"
header "^\[$"

color brightblue "\<[-]?[0-9]*([Ee][+-]?[0-9]+)?\>" "\<[-]?[0](\.[0-9]+)?\>"
color yellow ""(\\.|[^"])*"|'(\\.|[^'])*'|[a-zA-Z]+[a-zA-Z0-9]*"
color cyan "\<null\>"
color brightcyan "\<(true|false)\>"
color brightyellow "\"(\\"|[^"])*\"\s*:" "'(\'|[^'])*'\s*:" "(\[|,)\s*[a-zA-Z0-9]*\s*:"
color white "(:|\[|,|\])"
color magenta "\\u[0-9a-fA-F]{4}|\\[bfnrt'"/\\]"
color ,green "[[:space:]]+$"
color ,red " + +| + +"
NUMBER: "\<[-]?[0-9]*([Ee][+-]?[0-9]+)?\>" "\<[-]?[0](\.[0-9]+)?\>"
STRING: ""(\\.|[^"])*"|'(\\.|[^'])*'|[a-zA-Z]+[a-zA-Z0-9]*"
NULL: "\<null\>"
BOOLEAN: "\<(true|false)\>"
VARIABLE: "\"(\\"|[^"])*\"\s*:" "'(\'|[^'])*'\s*:" "(\[|,)\s*[a-zA-Z0-9]*\s*:"
PLAIN: "(:|\[|,|\])"
ESCAPE: "\\u[0-9a-fA-F]{4}|\\[bfnrt'"/\\]"
Loading

0 comments on commit 5cef3ba

Please sign in to comment.