|
18 | 18 |
|
19 | 19 | package com.xenomachina.argparser |
20 | 20 |
|
| 21 | +import com.xenomachina.common.Holder |
| 22 | +import com.xenomachina.common.orElse |
| 23 | +import com.xenomachina.text.term.codePointWidth |
| 24 | +import com.xenomachina.text.term.columnize |
| 25 | +import com.xenomachina.text.term.wrapText |
21 | 26 | import java.io.Writer |
22 | 27 | import kotlin.reflect.KProperty |
23 | 28 |
|
@@ -662,6 +667,9 @@ interface HelpFormatter { |
662 | 667 | val help: String) |
663 | 668 | } |
664 | 669 |
|
| 670 | +// TODO make verison in com.xenomachina.text public |
| 671 | +internal const val NBSP_CODEPOINT = 160 |
| 672 | + |
665 | 673 | /** |
666 | 674 | * Default implementation of [HelpFormatter]. Output is modelled after that of common UNIX utilities and looks |
667 | 675 | * something like this: |
@@ -708,6 +716,7 @@ class DefaultHelpFormatter( |
708 | 716 | ): String { |
709 | 717 | val sb = StringBuilder() |
710 | 718 | appendUsage(sb, columns, progName, values) |
| 719 | + sb.append("\n") |
711 | 720 |
|
712 | 721 | if (!prologue.isNullOrEmpty()) { |
713 | 722 | sb.append("\n") |
@@ -754,7 +763,7 @@ class DefaultHelpFormatter( |
754 | 763 | val left = value.usages.map { it.replace(' ', '\u00a0') }.joinToString(", ").wrapText(helpPos - indentWidth).prependIndent(indent) |
755 | 764 | val right = value.help.wrapText(columns - helpPos - 2 * indentWidth).prependIndent(indent) |
756 | 765 | sb.append(columnize(left, right, minWidths = intArrayOf(helpPos))) |
757 | | - sb.append("\n") |
| 766 | + sb.append("\n\n") |
758 | 767 | } |
759 | 768 | } |
760 | 769 | } |
@@ -857,213 +866,3 @@ open class UnexpectedOptionArgumentException(val optName: String) : |
857 | 866 | */ |
858 | 867 | open class UnexpectedPositionalArgumentException(val valueName: String?) : |
859 | 868 | SystemExitException("unexpected argument${if (valueName == null) "" else " after $valueName"}", 2) |
860 | | - |
861 | | -// TODO: move this to com.xenomachina.common |
862 | | - |
863 | | -/** |
864 | | - * a `Holder<T>?` can be used where one needs to be able to distinguish between having a T or not having a T, even |
865 | | - * when T is a nullable type. |
866 | | - * |
867 | | - * @property value the value being held |
868 | | - */ |
869 | | -data class Holder<T>(val value: T) |
870 | | - |
871 | | -/** |
872 | | - * Dereferences the [Holder] if non-null, otherwise returns the result of calling [fallback]. |
873 | | - */ |
874 | | -fun <T> Holder<T>?.orElse(fallback: () -> T): T { |
875 | | - if (this == null) { |
876 | | - return fallback() |
877 | | - } else { |
878 | | - return value |
879 | | - } |
880 | | -} |
881 | | - |
882 | | -// TODO: move all declarations below this point to com.xenomachina.text |
883 | | - |
884 | | -/** |
885 | | - * Performs the given [operation] on each line in this [String]. |
886 | | - */ |
887 | | -internal inline fun String.forEachLine(operation: (String) -> Unit) { |
888 | | - var index = 0 |
889 | | - while (true) { |
890 | | - val nextNewline = indexOf("\n", index) |
891 | | - if (nextNewline < 0) break |
892 | | - operation(substring(index, nextNewline + 1)) |
893 | | - index = nextNewline + 1 |
894 | | - } |
895 | | - operation(substring(index)) |
896 | | -} |
897 | | - |
898 | | -/** |
899 | | - * Performs the given [operation] on each Unicode codepoint in this [String]. |
900 | | - */ |
901 | | -internal inline fun String.forEachCodePoint(operation: (Int) -> Unit) { |
902 | | - val length = this.length |
903 | | - var offset = 0 |
904 | | - while (offset < length) { |
905 | | - val codePoint = this.codePointAt(offset) |
906 | | - operation(codePoint) |
907 | | - offset += Character.charCount(codePoint) |
908 | | - } |
909 | | -} |
910 | | - |
911 | | -internal fun StringBuilder.clear() { |
912 | | - this.setLength(0) |
913 | | -} |
914 | | - |
915 | | -internal val SPACE_WIDTH = 1 |
916 | | - |
917 | | -internal fun String.padTo(width: Int): String { |
918 | | - val sb = StringBuilder() |
919 | | - var lineWidth = 0 |
920 | | - forEachCodePoint { |
921 | | - if (it == '\n'.toInt()) { |
922 | | - while (lineWidth < width) { |
923 | | - sb.append(" ") |
924 | | - lineWidth += SPACE_WIDTH |
925 | | - } |
926 | | - sb.append("\n") |
927 | | - lineWidth = 0 |
928 | | - } else { |
929 | | - sb.appendCodePoint(it) |
930 | | - lineWidth += codePointWidth(it) |
931 | | - } |
932 | | - } |
933 | | - return sb.toString() |
934 | | -} |
935 | | - |
936 | | -internal val NBSP_CODEPOINT = 0xa0 |
937 | | - |
938 | | -internal fun String.wrapText(maxWidth: Int): String { |
939 | | - val sb = StringBuilder() |
940 | | - val word = StringBuilder() |
941 | | - var lineWidth = 0 |
942 | | - var wordWidth = 0 |
943 | | - fun handleSpace() { |
944 | | - if (wordWidth > 0) { |
945 | | - if (lineWidth > 0) { |
946 | | - sb.append(" ") |
947 | | - lineWidth += SPACE_WIDTH |
948 | | - } |
949 | | - sb.append(word) |
950 | | - lineWidth += wordWidth |
951 | | - word.clear() |
952 | | - wordWidth = 0 |
953 | | - } |
954 | | - } |
955 | | - forEachCodePoint { |
956 | | - if (Character.isSpaceChar(it) && it != NBSP_CODEPOINT) { |
957 | | - // space |
958 | | - handleSpace() |
959 | | - } else { |
960 | | - // non-space |
961 | | - val codepoint = if (it == NBSP_CODEPOINT) ' '.toInt() else it |
962 | | - val charWidth = codePointWidth(codepoint).toInt() |
963 | | - if (lineWidth > 0 && lineWidth + SPACE_WIDTH + wordWidth + charWidth > maxWidth) { |
964 | | - sb.append("\n") |
965 | | - lineWidth = 0 |
966 | | - } |
967 | | - if (lineWidth == 0 && lineWidth + SPACE_WIDTH + wordWidth + charWidth > maxWidth) { |
968 | | - // Eep! Word would be longer than line. Need to break it. |
969 | | - sb.append(word) |
970 | | - word.clear() |
971 | | - wordWidth = 0 |
972 | | - sb.append("\n") |
973 | | - lineWidth = 0 |
974 | | - } |
975 | | - word.appendCodePoint(codepoint) |
976 | | - wordWidth += charWidth |
977 | | - } |
978 | | - } |
979 | | - handleSpace() |
980 | | - |
981 | | - return sb.toString() |
982 | | -} |
983 | | - |
984 | | -/** |
985 | | - * Returns an estimated cell width of a Unicode code point when displayed on a monospace terminal. |
986 | | - * Possible return values are -1, 0, 1 or 2. Control characters (other than null) and Del return -1. |
987 | | - * |
988 | | - * This function is based on the public domain [wcwidth.c](https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c) |
989 | | - * written by Markus Kuhn. |
990 | | - */ |
991 | | -internal fun codePointWidth(ucs: Int): Byte { |
992 | | - // 8-bit control characters |
993 | | - if (ucs == 0) return 0 |
994 | | - if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) return -1 |
995 | | - |
996 | | - // Non-spacing characters. This is simulating the binary search of |
997 | | - // `uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B`. |
998 | | - if (ucs != 0x00AD) { // soft hyphen |
999 | | - val category = Character.getType(ucs).toByte() |
1000 | | - if (category == Character.ENCLOSING_MARK || // "Me" |
1001 | | - category == Character.NON_SPACING_MARK || // "Mn" |
1002 | | - category == Character.FORMAT || // "Cf" |
1003 | | - (0x1160 <= ucs && ucs <= 0x11FF) || // Hangul Jungseong & Jongseong |
1004 | | - ucs == 0x200B) // zero width space |
1005 | | - return 0 |
1006 | | - } |
1007 | | - |
1008 | | - // If we arrive here, ucs is not a combining or C0/C1 control character. |
1009 | | - return if (ucs >= 0x1100 && (ucs <= 0x115f || // Hangul Jamo init. consonants |
1010 | | - ucs == 0x2329 || ucs == 0x232a || |
1011 | | - (ucs >= 0x2e80 && ucs <= 0xa4cf && ucs != 0x303f) || // CJK ... Yi |
1012 | | - (ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables |
1013 | | - (ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compatibility Ideographs |
1014 | | - (ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms |
1015 | | - (ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compatibility Forms |
1016 | | - (ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms |
1017 | | - (ucs >= 0xffe0 && ucs <= 0xffe6) || |
1018 | | - (ucs >= 0x20000 && ucs <= 0x2fffd) || |
1019 | | - (ucs >= 0x30000 && ucs <= 0x3fffd))) |
1020 | | - 2 else 1 |
1021 | | -} |
1022 | | - |
1023 | | -internal fun String.codePointWidth(): Int { |
1024 | | - var result = 0 |
1025 | | - forEachCodePoint { |
1026 | | - result += codePointWidth(it) |
1027 | | - } |
1028 | | - return result |
1029 | | -} |
1030 | | - |
1031 | | -internal fun String.trimNewline(): String { |
1032 | | - if (endsWith('\n')) { |
1033 | | - return substring(0, length - 1) |
1034 | | - } else { |
1035 | | - return this |
1036 | | - } |
1037 | | -} |
1038 | | - |
1039 | | -internal fun columnize(vararg s: String, minWidths: IntArray? = null): String { |
1040 | | - val columns = Array(s.size) { mutableListOf<String>() } |
1041 | | - val widths = Array(s.size) { 0 } |
1042 | | - for (i in 0..s.size - 1) { |
1043 | | - if (minWidths != null && i < minWidths.size) { |
1044 | | - widths[i] = minWidths[i] |
1045 | | - } |
1046 | | - s[i].forEachLine { |
1047 | | - val cell = it.trimNewline() |
1048 | | - columns[i].add(cell) |
1049 | | - widths[i] = widths[i].coerceAtLeast(cell.codePointWidth()) |
1050 | | - } |
1051 | | - } |
1052 | | - val height = columns.maxBy { it.size }?.size ?: 0 |
1053 | | - val sb = StringBuilder() |
1054 | | - for (j in 0..height - 1) { |
1055 | | - var lineWidth = 0 |
1056 | | - var columnStart = 0 |
1057 | | - for (i in 0..columns.size - 1) { |
1058 | | - columns[i].getOrNull(j)?.let { cell -> |
1059 | | - for (k in 1..columnStart - lineWidth) sb.append(" ") |
1060 | | - lineWidth = columnStart |
1061 | | - sb.append(cell) |
1062 | | - lineWidth += cell.codePointWidth() |
1063 | | - } |
1064 | | - columnStart += widths[i] |
1065 | | - } |
1066 | | - sb.append("\n") |
1067 | | - } |
1068 | | - return sb.toString() |
1069 | | -} |
0 commit comments