From b844c63736fb92a3d357458b9c8a4bb8b020ad79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Jeschke?= Date: Wed, 18 Mar 2015 21:06:35 +0100 Subject: [PATCH 1/5] Adding a more versatile command line interface. --- .../rjeschke/txtmark/cmd/CmdArgument.java | 76 ++ .../rjeschke/txtmark/cmd/CmdLineParser.java | 691 ++++++++++++++++++ .../com/github/rjeschke/txtmark/cmd/Run.java | 142 ++++ .../txtmark/cmd/TxtmarkArguments.java | 37 + 4 files changed, 946 insertions(+) create mode 100644 src/main/java/com/github/rjeschke/txtmark/cmd/CmdArgument.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/cmd/CmdLineParser.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/cmd/Run.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/cmd/TxtmarkArguments.java diff --git a/src/main/java/com/github/rjeschke/txtmark/cmd/CmdArgument.java b/src/main/java/com/github/rjeschke/txtmark/cmd/CmdArgument.java new file mode 100644 index 0000000..8e51441 --- /dev/null +++ b/src/main/java/com/github/rjeschke/txtmark/cmd/CmdArgument.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2013-2015 René Jeschke + * + * 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 + * + * http://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. + */ +package com.github.rjeschke.txtmark.cmd; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation for command line parsing. + * + * @author René Jeschke (rene_jeschke@yahoo.de) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@interface CmdArgument +{ + /** + * Long name for argument. Default is 'none'. Either one or both of {@code l}, {@code s} need to be provided. + */ + String l() default ""; + + /** + * Short name (character) for argument. Default is 'none'. Either one or both of {@code l}, {@code s} need to be provided. + */ + char s() default '\0'; + + /** + * A description for this argument. Default is 'none'. + */ + String desc() default ""; + + /** + * List item separator. Default is ','. + */ + char listSep() default ','; + + /** + * Class for List-type arguments. Default is {@code String.class}. + */ + Class listType() default String.class; + + /** + * Set to {@code true} if this is a switch. Requires a {@code boolean} field which gets set to {@code true} when this argument is provided. + */ + boolean isSwitch() default false; + + /** + * Set to {@code true} if this is a required argument. + */ + boolean required() default false; + + /** + * Set to {@code true} to set this as a catch-all argument. Requires a {@code List} field and will parse all arguments following this switch into the list. + */ + boolean catchAll() default false; + + /** + * Set to {@code false} to disable automatic default value printing for this argument. + */ + boolean printDefault() default true; +} diff --git a/src/main/java/com/github/rjeschke/txtmark/cmd/CmdLineParser.java b/src/main/java/com/github/rjeschke/txtmark/cmd/CmdLineParser.java new file mode 100644 index 0000000..f51b480 --- /dev/null +++ b/src/main/java/com/github/rjeschke/txtmark/cmd/CmdLineParser.java @@ -0,0 +1,691 @@ +/* + * Copyright (C) 2013-2015 René Jeschke + * + * 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 + * + * http://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. + */ +package com.github.rjeschke.txtmark.cmd; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +/** + * Generic command line parser. + * + * This is a copy from neetutils-base. + * + * @author René Jeschke (rene_jeschke@yahoo.de) + */ +final class CmdLineParser +{ + private CmdLineParser() + { + // meh! + } + + enum Type + { + UNSUPPORTED, STRING, BYTE, SHORT, INT, LONG, FLOAT, DOUBLE, LIST, BOOL; + } + + final static HashMap, Type> TYPE_MAP = new HashMap, Type>(); + final static Class[] TYPE_CLASS_LIST = Colls.> objArray(String.class, byte.class, + Byte.class, short.class, Short.class, int.class, + Integer.class, long.class, Long.class, float.class, + Float.class, double.class, Double.class, List.class, + Boolean.class, boolean.class); + final static Type[] TYPE_TYPE_LIST = Colls.objArray(Type.STRING, Type.BYTE, Type.BYTE, + Type.SHORT, + Type.SHORT, Type.INT, Type.INT, Type.LONG, Type.LONG, + Type.FLOAT, Type.FLOAT, Type.DOUBLE, Type.DOUBLE, + Type.LIST, Type.BOOL, Type.BOOL); + + final static HashSet BOOL_TRUE = new HashSet(Colls.list("on", "true", "yes")); + final static HashSet BOOL_FALSE = new HashSet(Colls.list("off", "false", "no")); + + static + { + for (int i = 0; i < TYPE_CLASS_LIST.length; i++) + { + TYPE_MAP.put(TYPE_CLASS_LIST[i], TYPE_TYPE_LIST[i]); + } + } + + static Type getTypeFor(final Class clazz) + { + final Type type = TYPE_MAP.get(clazz); + + if (type != null) + { + return type; + } + + if (Classes.implementsInterface(clazz, List.class)) + { + return Type.LIST; + } + + return Type.UNSUPPORTED; + } + + static String defaultToString(final Object value, final Type type, final Arg arg) + { + if (value == null || arg.isSwitch || arg.catchAll || !arg.printDefault) + { + return null; + } + + if (type == Type.LIST) + { + final List list = (List)value; + + if (list.isEmpty()) + { + return null; + } + + final StringBuilder sb = new StringBuilder(); + final Once once = Once.of("", Character.toString(arg.itemSep)); + for (final Object o : list) + { + sb.append(once.get()); + sb.append(o.toString()); + } + return sb.toString(); + } + + return value.toString(); + } + + private static void parseArgs(final Object[] objs, final List allArgs, final HashMap shortArgs, + final HashMap longArgs) + throws IOException + { + for (final Object obj : objs) + { + final Class cl = obj.getClass(); + final Field[] fields = cl.getDeclaredFields(); + + for (final Field f : fields) + { + if (f.isAnnotationPresent(CmdArgument.class)) + { + final Arg arg = new Arg(f.getAnnotation(CmdArgument.class), obj, f); + + if (arg.type == Type.UNSUPPORTED) + { + throw new IOException("Unsupported parameter type: " + f.getType().getCanonicalName() + + " for: " + arg); + } + + if (arg.listType == Type.UNSUPPORTED || arg.listType == Type.LIST) + { + throw new IOException("Unsupported list type: " + f.getType().getCanonicalName() + " for: " + + arg); + } + + if (Strings.isEmpty(arg.s) && Strings.isEmpty(arg.l)) + { + throw new IOException("Missing parameter name"); + } + + if (!Strings.isEmpty(arg.s)) + { + if (shortArgs.containsKey(arg.s)) + { + throw new IOException("Duplicate short argument: -" + arg.s); + } + shortArgs.put(arg.s, arg); + } + + if (!Strings.isEmpty(arg.l)) + { + if (longArgs.containsKey(arg.l)) + { + throw new IOException("Duplicate long argument: --" + arg.l); + } + longArgs.put(arg.l, arg); + } + + if (arg.isCatchAll() && arg.type != Type.LIST) + { + throw new IOException("Parameter '" + arg + "' requires a List field."); + } + + if (arg.isSwitch && arg.type != Type.BOOL) + { + throw new IOException("Parameter '" + arg + "' requires a Boolean/boolean field."); + } + + allArgs.add(arg); + } + } + } + } + + /** + * Generates a formatted help (Unix-style) for the given argument objects. + * + * @param columnWidth + * Maximum column width. Words get wrapped at spaces. + * @param sort + * Set {@code true} to sort arguments before printing. + * @param objs + * One or more objects with annotated public fields. + * @return The formatted argument help text. + * @throws IOException + * if a parsing error occurred. + * @see CmdArgument + */ + public static String generateHelp(final int columnWidth, final boolean sort, final Object... objs) + throws IOException + { + final List allArgs = Colls.list(); + final HashMap shortArgs = new HashMap(); + final HashMap longArgs = new HashMap(); + + parseArgs(objs, allArgs, shortArgs, longArgs); + + int minArgLen = 0; + + for (final Arg a : allArgs) + { + int len = a.toString().length(); + if (!a.isSwitch) + { + ++len; + len += a.getResolvedType().toString().length(); + if (a.isCatchAll()) + { + ++len; + } + else if (a.isList()) + { + len += 6; + } + } + minArgLen = Math.max(minArgLen, len); + } + minArgLen += 2; + if (sort) + { + Collections.sort(allArgs); + } + + final StringBuilder sb = new StringBuilder(); + + for (final Arg a : allArgs) + { + final StringBuilder line = new StringBuilder(); + line.append(' '); + line.append(a); + if (!a.isSwitch) + { + line.append(' '); + line.append(a.getResolvedType().toString().toLowerCase()); + if (a.isCatchAll()) + { + line.append('s'); + } + else if (a.isList()) + { + line.append('['); + line.append(a.itemSep); + line.append("...]"); + } + } + while (line.length() < minArgLen) + { + line.append(' '); + } + + line.append(':'); + + final StringBuilder desc = new StringBuilder(a.desc.trim()); + + final String defVal = defaultToString(a.safeFieldGet(), a.type, a); + + if (defVal != null) + { + desc.append(" Default is: '"); + desc.append(defVal); + desc.append("'."); + } + + final List toks = Strings.split(desc.toString(), ' '); + + for (final String s : toks) + { + if (line.length() + s.length() + 1 > columnWidth) + { + sb.append(line); + sb.append('\n'); + line.setLength(0); + while (line.length() <= minArgLen) + { + line.append(' '); + } + line.append(' '); + } + line.append(' '); + line.append(s); + } + + if (line.length() > minArgLen) + { + sb.append(line); + sb.append('\n'); + } + } + + return sb.toString(); + } + + /** + * Parses command line arguments. + * + * @param args + * Array of arguments, like the ones provided by + * {@code void main(String[] args)} + * @param objs + * One or more objects with annotated public fields. + * @return A {@code List} containing all unparsed arguments (i.e. arguments + * that are no switches) + * @throws IOException + * if a parsing error occurred. + * @see CmdArgument + */ + public static List parse(final String[] args, final Object... objs) throws IOException + { + final List ret = Colls.list(); + + final List allArgs = Colls.list(); + final HashMap shortArgs = new HashMap(); + final HashMap longArgs = new HashMap(); + + parseArgs(objs, allArgs, shortArgs, longArgs); + + for (int i = 0; i < args.length; i++) + { + final String s = args[i]; + + final Arg a; + + if (s.startsWith("--")) + { + a = longArgs.get(s.substring(2)); + if (a == null) + { + throw new IOException("Unknown switch: " + s); + } + } + else if (s.startsWith("-")) + { + a = shortArgs.get(s.substring(1)); + if (a == null) + { + throw new IOException("Unknown switch: " + s); + } + } + else + { + a = null; + ret.add(s); + } + + if (a != null) + { + if (a.isSwitch) + { + a.setField("true"); + } + else + { + if (i + 1 >= args.length) + { + System.out.println("Missing parameter for: " + s); + } + if (a.isCatchAll()) + { + final List ca = Colls.list(); + for (++i; i < args.length; ++i) + { + ca.add(args[i]); + } + a.setCatchAll(ca); + } + else + { + ++i; + a.setField(args[i]); + } + } + a.setPresent(); + } + } + + for (final Arg a : allArgs) + { + if (!a.isOk()) + { + throw new IOException("Missing mandatory argument: " + a); + } + } + + return ret; + } + + private static class Arg implements Comparable + { + final String s; + final String l; + final String id; + final String desc; + final char itemSep; + final boolean isSwitch; + final boolean required; + final boolean catchAll; + final boolean printDefault; + final Type type; + final Type listType; + boolean present = false; + final Object object; + final Field field; + + public Arg(final CmdArgument arg, final Object obj, final Field field) + { + this.s = arg.s() == 0 ? "" : Character.toString(arg.s()); + this.l = arg.l(); + this.desc = arg.desc(); + this.isSwitch = arg.isSwitch(); + this.required = arg.required(); + this.catchAll = arg.catchAll(); + this.itemSep = arg.listSep(); + this.printDefault = arg.printDefault(); + this.id = this.s + "/" + this.l; + + this.object = obj; + this.field = field; + this.type = getTypeFor(this.field.getType()); + this.listType = getTypeFor(arg.listType()); + } + + public Type getResolvedType() + { + return this.isList() ? this.listType : this.type; + } + + public boolean isCatchAll() + { + return this.catchAll; + } + + public boolean isList() + { + return this.type == Type.LIST; + } + + public void setCatchAll(final List list) throws IOException + { + this.setListField(list); + } + + public void setListField(final List list) throws IOException + { + try + { + if (this.listType == Type.STRING) + { + this.field.set(this.object, list); + } + else + { + final List temp = Colls.list(); + for (final String i : list) + { + temp.add(this.toObject(i, this.listType)); + } + this.field.set(this.object, temp); + } + } + catch (final IllegalArgumentException e) + { + throw new IOException("Failed to write value", e); + } + catch (final IllegalAccessException e) + { + throw new IOException("Failed to write value", e); + } + } + + Object safeFieldGet() + { + try + { + return this.field.get(this.object); + } + catch (final Exception e) + { + return null; + } + } + + private Object toObject(final String value, final Type type) throws IOException + { + try + { + switch (type) + { + case STRING: + return value; + case BYTE: + return Byte.parseByte(value); + case SHORT: + return Short.parseShort(value); + case INT: + return Integer.parseInt(value); + case LONG: + return Long.parseLong(value); + case FLOAT: + return Float.parseFloat(value); + case DOUBLE: + return Double.parseDouble(value); + case BOOL: + if (BOOL_TRUE.contains(value.toLowerCase())) + { + return true; + } + if (BOOL_FALSE.contains(value.toLowerCase())) + { + return false; + } + throw new IOException("Illegal bool value for:" + this.toString()); + default: + throw new IOException("Illegal type: " + type.toString().toLowerCase()); + } + } + catch (final Throwable t) + { + throw new IOException("Parsing error for: " + this.toString() + "; '" + value + "'", t); + } + } + + public void setField(final String value) throws IOException + { + try + { + if (this.isList()) + { + this.setListField(Strings.split(value, this.itemSep)); + } + else + { + this.field.set(this.object, this.toObject(value, this.type)); + } + } + catch (final IllegalArgumentException e) + { + throw new IOException("Failed to write field: " + this.field.getName(), e); + } + catch (final IllegalAccessException e) + { + throw new IOException("Failed to write field: " + this.field.getName(), e); + } + } + + public void setPresent() + { + this.present = true; + } + + public boolean isOk() + { + return !this.required || this.present; + } + + @Override + public int hashCode() + { + return this.id.hashCode(); + } + + @Override + public boolean equals(final Object obj) + { + if (obj instanceof Arg) + { + return this.id.equals(((Arg)obj).id); + } + return false; + } + + @Override + public String toString() + { + if (Strings.isEmpty(this.s)) + { + return " --" + this.l; + } + if (Strings.isEmpty(this.l)) + { + return "-" + this.s; + } + return "-" + this.s + ", --" + this.l; + } + + @Override + public int compareTo(final Arg o) + { + final String a = Strings.isEmpty(this.s) ? this.l : this.s; + final String b = Strings.isEmpty(o.s) ? o.l : o.s; + return a.compareTo(b); + } + } + + private static class Once + { + private final T first; + private final T next; + private boolean isFirst = true; + + public Once(final T first, final T next) + { + this.first = first; + this.next = next; + } + + public static Once of(final T first, final T next) + { + return new Once(first, next); + } + + public T get() + { + if (this.isFirst) + { + this.isFirst = false; + return this.first; + } + + return this.next; + } + } + + private final static class Colls + { + final static T[] objArray(final T... ts) + { + return ts; + } + + final static List list(final A... coll) + { + final List ret = new ArrayList(coll.length); + for (int i = 0; i < coll.length; i++) + { + ret.add(coll[i]); + } + return ret; + } + } + + private final static class Classes + { + final static boolean implementsInterface(final Class clazz, final Class interfce) + { + for (final Class c : clazz.getInterfaces()) + { + if (c.equals(interfce)) + { + return true; + } + } + + return false; + } + } + + private final static class Strings + { + public final static boolean isEmpty(final String str) + { + return str == null || str.isEmpty(); + } + + public final static List split(final String str, final char ch) + { + final List ret = Colls.list(); + + if (str != null) + { + int s = 0, e = 0; + while (e < str.length()) + { + if (str.charAt(e) == ch) + { + ret.add(str.substring(s, e)); + s = e + 1; + } + e++; + } + ret.add(str.substring(s, e)); + } + + return ret; + } + } +} diff --git a/src/main/java/com/github/rjeschke/txtmark/cmd/Run.java b/src/main/java/com/github/rjeschke/txtmark/cmd/Run.java new file mode 100644 index 0000000..9ec4612 --- /dev/null +++ b/src/main/java/com/github/rjeschke/txtmark/cmd/Run.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2013-2015 René Jeschke + * + * 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 + * + * http://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. + */ +package com.github.rjeschke.txtmark.cmd; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; + +import com.github.rjeschke.txtmark.Configuration; +import com.github.rjeschke.txtmark.Processor; + +public class Run +{ + public static void printUsage() + { + try + { + System.out.println("Usage: txtmark [options] [input-file]"); + System.out.println("Options:"); + System.out.println(CmdLineParser.generateHelp(80, false, new TxtmarkArguments())); + } + catch (final IOException e) + { + // + } + } + + public static void main(final String[] args) + { + final TxtmarkArguments ta = new TxtmarkArguments(); + List rest = new ArrayList(); + + boolean parseError = false; + try + { + rest = CmdLineParser.parse(args, ta); + } + catch (final IOException e) + { + parseError = true; + } + + if (ta.printHelp || parseError) + { + printUsage(); + System.exit(parseError ? 1 : 0); + } + + final Configuration.Builder cfgBuilder = Configuration.builder(); + cfgBuilder.setEncoding(ta.encoding) + .setEnablePanicMode(ta.panicMode) + .setSafeMode(ta.safeMode); + + if (ta.forceExtendedProfile) + { + cfgBuilder.forceExtentedProfile(); + } + + final Configuration config = cfgBuilder.build(); + boolean processOk = true; + InputStream input = null; + Writer output = null; + try + { + final String inFile = rest.isEmpty() ? "--" : rest.get(0); + final String outFile = ta.outFile; + + if (inFile.equals("--")) + { + input = System.in; + } + else + { + input = new FileInputStream(inFile); + } + + final String result = Processor.process(input, config); + + if (outFile == null) + { + output = new OutputStreamWriter(System.out, ta.encoding); + } + else + { + output = new OutputStreamWriter(new FileOutputStream(outFile), ta.encoding); + } + + output.write(result); + } + catch (final IOException e) + { + processOk = false; + System.err.println("Exception: " + e.toString()); + e.printStackTrace(System.err); + } + finally + { + if (input != null) + { + try + { + input.close(); + } + catch (final IOException e) + { + // ignore + } + } + if (output != null) + { + try + { + output.close(); + } + catch (final IOException e) + { + // ignore + } + } + } + + System.exit(processOk ? 0 : 1); + } +} diff --git a/src/main/java/com/github/rjeschke/txtmark/cmd/TxtmarkArguments.java b/src/main/java/com/github/rjeschke/txtmark/cmd/TxtmarkArguments.java new file mode 100644 index 0000000..732a4d2 --- /dev/null +++ b/src/main/java/com/github/rjeschke/txtmark/cmd/TxtmarkArguments.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2013-2015 René Jeschke + * + * 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 + * + * http://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. + */ +package com.github.rjeschke.txtmark.cmd; + +class TxtmarkArguments +{ + @CmdArgument(l = "help", s = 'h', isSwitch = true, desc = "prints a summary of command line arguments.") + public boolean printHelp = false; + + @CmdArgument(l = "extended", isSwitch = true, desc = "forces extended profile") + public boolean forceExtendedProfile = false; + + @CmdArgument(l = "panic-mode", isSwitch = true, desc = "enables panic mode") + public boolean panicMode = false; + + @CmdArgument(l = "safe-mode", isSwitch = true, desc = "enables safe mode") + public boolean safeMode = false; + + @CmdArgument(l = "encoding", desc = "sets the IO encoding.") + public String encoding = "UTF-8"; + + @CmdArgument(l = "out-file", s = 'o', desc = "specifies output filename") + public String outFile = null; +} From 79b0adccb96e1ed41ebc18a0e4ed125c8c07737a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Jeschke?= Date: Wed, 18 Mar 2015 21:33:43 +0100 Subject: [PATCH 2/5] Added external highlighter bindings. --- .../txtmark/cmd/CodeBlockEmitter.java | 66 +++++++ .../github/rjeschke/txtmark/cmd/HlUtils.java | 163 ++++++++++++++++++ .../com/github/rjeschke/txtmark/cmd/Run.java | 17 +- .../txtmark/cmd/TxtmarkArguments.java | 7 +- 4 files changed, 248 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/github/rjeschke/txtmark/cmd/CodeBlockEmitter.java create mode 100644 src/main/java/com/github/rjeschke/txtmark/cmd/HlUtils.java diff --git a/src/main/java/com/github/rjeschke/txtmark/cmd/CodeBlockEmitter.java b/src/main/java/com/github/rjeschke/txtmark/cmd/CodeBlockEmitter.java new file mode 100644 index 0000000..22a06f4 --- /dev/null +++ b/src/main/java/com/github/rjeschke/txtmark/cmd/CodeBlockEmitter.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2015 René Jeschke + * + * 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 + * + * http://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. + */ +package com.github.rjeschke.txtmark.cmd; + +import java.io.IOException; +import java.util.List; + +import com.github.rjeschke.txtmark.BlockEmitter; + +final class CodeBlockEmitter implements BlockEmitter +{ + private final String encoding; + private final String program; + + public CodeBlockEmitter(final String encoding, final String program) + { + this.encoding = encoding; + this.program = program; + } + + private static void append(final StringBuilder out, final List lines) + { + out.append("
");
+        for (final String l : lines)
+        {
+            HlUtils.escapedAdd(out, l);
+            out.append('\n');
+        }
+        out.append("
"); + } + + @Override + public void emitBlock(final StringBuilder out, final List lines, final String meta) + { + if (meta == null || meta.isEmpty()) + { + append(out, lines); + } + else + { + try + { + out.append(HlUtils.highlight(lines, meta, this.program, this.encoding)); + out.append('\n'); + } + catch (final IOException e) + { + // Ignore or do something, still, pump out the lines + append(out, lines); + } + } + } +} diff --git a/src/main/java/com/github/rjeschke/txtmark/cmd/HlUtils.java b/src/main/java/com/github/rjeschke/txtmark/cmd/HlUtils.java new file mode 100644 index 0000000..ffcb807 --- /dev/null +++ b/src/main/java/com/github/rjeschke/txtmark/cmd/HlUtils.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2015 René Jeschke + * + * 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 + * + * http://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. + */ +package com.github.rjeschke.txtmark.cmd; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +final class HlUtils +{ + final static AtomicInteger IN_COUNT = new AtomicInteger(0); + final static AtomicInteger OUT_COUNT = new AtomicInteger(0); + final static long ID = System.nanoTime(); + + public static String highlight(final List lines, final String meta, final String prog, final String encoding) + throws IOException + { + final File tmpIn = new File(System.getProperty("java.io.tmpdir"), + String.format("txtmark_code_%d_%d.in", ID, IN_COUNT.incrementAndGet())); + final File tmpOut = new File(System.getProperty("java.io.tmpdir"), + String.format("txtmark_code_%d_%d.out", ID, OUT_COUNT.incrementAndGet())); + + try + { + + final Writer w = new OutputStreamWriter(new FileOutputStream(tmpIn), encoding); + + try + { + for (final String s : lines) + { + w.write(s); + w.write('\n'); + } + } + finally + { + w.close(); + } + + final List command = new ArrayList(); + + command.add(prog); + command.add(meta); + command.add(tmpIn.getAbsolutePath()); + command.add(tmpOut.getAbsolutePath()); + + final ProcessBuilder pb = new ProcessBuilder(command); + final Process p = pb.start(); + final InputStream pIn = p.getInputStream(); + final byte[] buffer = new byte[2048]; + + int exitCode = 0; + for (;;) + { + if (pIn.available() > 0) + { + pIn.read(buffer); + } + try + { + exitCode = p.exitValue(); + } + catch (final IllegalThreadStateException itse) + { + continue; + } + break; + } + + if (exitCode == 0) + { + final Reader r = new InputStreamReader(new FileInputStream(tmpOut), encoding); + try + { + final StringBuilder sb = new StringBuilder(); + for (;;) + { + final int c = r.read(); + if (c >= 0) + { + sb.append((char)c); + } + else + { + break; + } + } + return sb.toString(); + } + finally + { + r.close(); + } + } + + throw new IOException("Exited with exit code: " + exitCode); + } + finally + { + tmpIn.delete(); + tmpOut.delete(); + } + } + + public static void escapedAdd(final StringBuilder sb, final String str) + { + for (int i = 0; i < str.length(); i++) + { + final char ch = str.charAt(i); + if (ch < 33 || Character.isWhitespace(ch) || Character.isSpaceChar(ch)) + { + sb.append(' '); + } + else + { + switch (ch) + { + case '"': + sb.append("""); + break; + case '\'': + sb.append("'"); + break; + case '<': + sb.append("<"); + break; + case '>': + sb.append(">"); + break; + case '&': + sb.append("&"); + break; + default: + sb.append(ch); + break; + } + } + } + } +} diff --git a/src/main/java/com/github/rjeschke/txtmark/cmd/Run.java b/src/main/java/com/github/rjeschke/txtmark/cmd/Run.java index 9ec4612..fa2060d 100644 --- a/src/main/java/com/github/rjeschke/txtmark/cmd/Run.java +++ b/src/main/java/com/github/rjeschke/txtmark/cmd/Run.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013-2015 René Jeschke + * Copyright (C) 2015 René Jeschke * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ package com.github.rjeschke.txtmark.cmd; +import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -27,9 +28,9 @@ import java.util.List; import com.github.rjeschke.txtmark.Configuration; import com.github.rjeschke.txtmark.Processor; -public class Run +public final class Run { - public static void printUsage() + private final static void printUsage() { try { @@ -74,6 +75,16 @@ public class Run cfgBuilder.forceExtentedProfile(); } + if (ta.highlighter != null && !ta.highlighter.isEmpty()) + { + if (!new File(ta.highlighter).exists()) + { + System.err.println("Program '" + ta.highlighter + "' not found"); + System.exit(1); + } + cfgBuilder.setCodeBlockEmitter(new CodeBlockEmitter(ta.encoding, ta.highlighter)); + } + final Configuration config = cfgBuilder.build(); boolean processOk = true; InputStream input = null; diff --git a/src/main/java/com/github/rjeschke/txtmark/cmd/TxtmarkArguments.java b/src/main/java/com/github/rjeschke/txtmark/cmd/TxtmarkArguments.java index 732a4d2..baa90ba 100644 --- a/src/main/java/com/github/rjeschke/txtmark/cmd/TxtmarkArguments.java +++ b/src/main/java/com/github/rjeschke/txtmark/cmd/TxtmarkArguments.java @@ -15,7 +15,7 @@ */ package com.github.rjeschke.txtmark.cmd; -class TxtmarkArguments +final class TxtmarkArguments { @CmdArgument(l = "help", s = 'h', isSwitch = true, desc = "prints a summary of command line arguments.") public boolean printHelp = false; @@ -32,6 +32,9 @@ class TxtmarkArguments @CmdArgument(l = "encoding", desc = "sets the IO encoding.") public String encoding = "UTF-8"; - @CmdArgument(l = "out-file", s = 'o', desc = "specifies output filename") + @CmdArgument(l = "out-file", s = 'o', desc = "specifies the output filename, writes to stdout otherwise") public String outFile = null; + + @CmdArgument(l = "highlighter", desc = "specifies a program [meta in-file outfile] that should be used for highlighting fenced code blocks") + public String highlighter = null; } From 04e3c74f3979fd67104dd5829c597783d9503876 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Jeschke?= Date: Wed, 18 Mar 2015 21:36:47 +0100 Subject: [PATCH 3/5] Last config switch added. --- src/main/java/com/github/rjeschke/txtmark/cmd/Run.java | 9 ++++++--- .../github/rjeschke/txtmark/cmd/TxtmarkArguments.java | 3 +++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/github/rjeschke/txtmark/cmd/Run.java b/src/main/java/com/github/rjeschke/txtmark/cmd/Run.java index fa2060d..876fb48 100644 --- a/src/main/java/com/github/rjeschke/txtmark/cmd/Run.java +++ b/src/main/java/com/github/rjeschke/txtmark/cmd/Run.java @@ -65,16 +65,18 @@ public final class Run System.exit(parseError ? 1 : 0); } + // Build configuration from command line arguments final Configuration.Builder cfgBuilder = Configuration.builder(); cfgBuilder.setEncoding(ta.encoding) .setEnablePanicMode(ta.panicMode) - .setSafeMode(ta.safeMode); - + .setSafeMode(ta.safeMode) + .setAllowSpacesInFencedCodeBlockDelimiters(!ta.noFencedSpaced); + // Check for extended profile if (ta.forceExtendedProfile) { cfgBuilder.forceExtentedProfile(); } - + // Connect highlighter if any if (ta.highlighter != null && !ta.highlighter.isEmpty()) { if (!new File(ta.highlighter).exists()) @@ -85,6 +87,7 @@ public final class Run cfgBuilder.setCodeBlockEmitter(new CodeBlockEmitter(ta.encoding, ta.highlighter)); } + // Ready for action final Configuration config = cfgBuilder.build(); boolean processOk = true; InputStream input = null; diff --git a/src/main/java/com/github/rjeschke/txtmark/cmd/TxtmarkArguments.java b/src/main/java/com/github/rjeschke/txtmark/cmd/TxtmarkArguments.java index baa90ba..f379e9f 100644 --- a/src/main/java/com/github/rjeschke/txtmark/cmd/TxtmarkArguments.java +++ b/src/main/java/com/github/rjeschke/txtmark/cmd/TxtmarkArguments.java @@ -29,6 +29,9 @@ final class TxtmarkArguments @CmdArgument(l = "safe-mode", isSwitch = true, desc = "enables safe mode") public boolean safeMode = false; + @CmdArgument(l = "no-fenced-spaces", isSwitch = true, desc = "disables spaces in fenced code block delimiters") + public boolean noFencedSpaced = false; + @CmdArgument(l = "encoding", desc = "sets the IO encoding.") public String encoding = "UTF-8"; From 9632ca6c83967c3e40193e8eac0139e41eb151aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Jeschke?= Date: Wed, 18 Mar 2015 22:24:20 +0100 Subject: [PATCH 4/5] Added python bootstrapping tool. --- .gitignore | 1 + bootstrap.py/txtmark.py | 106 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 bootstrap.py/txtmark.py diff --git a/.gitignore b/.gitignore index 793dca3..9a2b6b8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ target/ *~ *.releaseBackup release.properties +*.pyc diff --git a/bootstrap.py/txtmark.py b/bootstrap.py/txtmark.py new file mode 100644 index 0000000..c45ee35 --- /dev/null +++ b/bootstrap.py/txtmark.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 :et:ts=4:sts=4 +# +# Copyright 2015 René Jeschke +# +# 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 +# +# http://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. + +import os +import re +import errno +import urllib2 +import sys +import subprocess + +group_id = "com.github.rjeschke" +artifact = "txtmark" +jar_dest = os.path.join(os.path.expanduser("~"), ".txtmark_jar", "txtmark.jar") + +oss_snapshots = "https://oss.sonatype.org/content/repositories/snapshots" +maven_repo1 = "https://repo1.maven.org/maven2" + +snap_url = oss_snapshots + "/" + group_id.replace(".", "/") + "/" + artifact +rel_url = maven_repo1 + "/" + group_id.replace(".", "/") + "/" + artifact + +def mkdirs(path): + """mkdir -p""" + try: + os.makedirs(path) + except OSError as e: + if e.errno != errno.EEXIST or not os.path.isdir(path): + raise + + +def read_mvn_infos(url): + response = urllib2.urlopen(url + "/maven-metadata.xml") + latest = None + version = None + last_modified = 0 + for line in response.read().split("\n"): + line = line.strip() + if re.match("^.+$", line): + latest = line[8:-9] + elif re.match("^.+$", line): + last_modified = long(line[13:-14]) + elif not latest and re.match("^.+$", line): + version = line[9:-10] + + if latest: + return [latest, last_modified] + return [version, last_modified] + + +def get_snapshot_version(url, version): + response = urllib2.urlopen(url + "/maven-metadata.xml") + timestamp = None + build_number = 0 + for line in response.read().split("\n"): + line = line.strip() + if not timestamp and re.match("^.*$", line): + timestamp = line[11:-12] + elif build_number == 0 and re.match("^.*$", line): + build_number = int(line[13:-14]) + return url + "/" + artifact + "-" + version[:version.find("-")] + "-" + timestamp + "-" + str(build_number) + ".jar" + + +def download(is_snap, version): + u = None + if is_snap: + u = get_snapshot_version(snap_url + "/" + version, version) + else: + u = rel_url + "/" + version + "/" + artifact + "-" + version + ".jar" + + response = urllib2.urlopen(u) + with open(jar_dest, "wb") as fd: + fd.write(response.read()) + + +def fetch_artifact(): + if not os.path.exists(jar_dest): + mkdirs(os.path.dirname(jar_dest)) + rel = read_mvn_infos(rel_url) + snp = read_mvn_infos(snap_url) + + if snp[1] > rel[1]: + download(True, snp[0]) + else: + download(False, rel[0]) + + +if __name__ == "__main__": + fetch_artifact() + + cmd = ["java", "-cp", jar_dest, "com.github.rjeschke.txtmark.cmd.Run"] + cmd.extend(sys.argv[1:]) + + exit(subprocess.call(cmd)) From 878f37a51a1f611269121dcdcc733909542a18ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Jeschke?= Date: Wed, 18 Mar 2015 22:42:26 +0100 Subject: [PATCH 5/5] Added link to neetutils-base. --- .../rjeschke/txtmark/cmd/CmdArgument.java | 18 +++++++++++++----- .../rjeschke/txtmark/cmd/CmdLineParser.java | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/github/rjeschke/txtmark/cmd/CmdArgument.java b/src/main/java/com/github/rjeschke/txtmark/cmd/CmdArgument.java index 8e51441..296b997 100644 --- a/src/main/java/com/github/rjeschke/txtmark/cmd/CmdArgument.java +++ b/src/main/java/com/github/rjeschke/txtmark/cmd/CmdArgument.java @@ -23,6 +23,8 @@ import java.lang.annotation.Target; /** * Annotation for command line parsing. * + * This is a copy from {@link https://github.com/rjeschke/neetutils-base}. + * * @author René Jeschke (rene_jeschke@yahoo.de) */ @Retention(RetentionPolicy.RUNTIME) @@ -30,12 +32,14 @@ import java.lang.annotation.Target; @interface CmdArgument { /** - * Long name for argument. Default is 'none'. Either one or both of {@code l}, {@code s} need to be provided. + * Long name for argument. Default is 'none'. Either one or both of + * {@code l}, {@code s} need to be provided. */ String l() default ""; /** - * Short name (character) for argument. Default is 'none'. Either one or both of {@code l}, {@code s} need to be provided. + * Short name (character) for argument. Default is 'none'. Either one or + * both of {@code l}, {@code s} need to be provided. */ char s() default '\0'; @@ -55,7 +59,8 @@ import java.lang.annotation.Target; Class listType() default String.class; /** - * Set to {@code true} if this is a switch. Requires a {@code boolean} field which gets set to {@code true} when this argument is provided. + * Set to {@code true} if this is a switch. Requires a {@code boolean} field + * which gets set to {@code true} when this argument is provided. */ boolean isSwitch() default false; @@ -65,12 +70,15 @@ import java.lang.annotation.Target; boolean required() default false; /** - * Set to {@code true} to set this as a catch-all argument. Requires a {@code List} field and will parse all arguments following this switch into the list. + * Set to {@code true} to set this as a catch-all argument. Requires a + * {@code List} field and will parse all arguments following this switch + * into the list. */ boolean catchAll() default false; /** - * Set to {@code false} to disable automatic default value printing for this argument. + * Set to {@code false} to disable automatic default value printing for this + * argument. */ boolean printDefault() default true; } diff --git a/src/main/java/com/github/rjeschke/txtmark/cmd/CmdLineParser.java b/src/main/java/com/github/rjeschke/txtmark/cmd/CmdLineParser.java index f51b480..e98b181 100644 --- a/src/main/java/com/github/rjeschke/txtmark/cmd/CmdLineParser.java +++ b/src/main/java/com/github/rjeschke/txtmark/cmd/CmdLineParser.java @@ -26,7 +26,7 @@ import java.util.List; /** * Generic command line parser. * - * This is a copy from neetutils-base. + * This is a copy from {@link https://github.com/rjeschke/neetutils-base}. * * @author René Jeschke (rene_jeschke@yahoo.de) */