You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Styliser.java 27KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  1. /*
  2. * Copyright (c) 2006-2015 DMDirc Developers
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy
  5. * of this software and associated documentation files (the "Software"), to deal
  6. * in the Software without restriction, including without limitation the rights
  7. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. * copies of the Software, and to permit persons to whom the Software is
  9. * furnished to do so, subject to the following conditions:
  10. *
  11. * The above copyright notice and this permission notice shall be included in
  12. * all copies or substantial portions of the Software.
  13. *
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  20. * SOFTWARE.
  21. */
  22. package com.dmdirc.ui.messages;
  23. import com.dmdirc.interfaces.Connection;
  24. import com.dmdirc.interfaces.config.AggregateConfigProvider;
  25. import com.dmdirc.interfaces.config.ConfigChangeListener;
  26. import com.dmdirc.util.colours.Colour;
  27. import java.util.Locale;
  28. import java.util.regex.Pattern;
  29. import javax.annotation.Nullable;
  30. import static com.google.common.base.Preconditions.checkArgument;
  31. /**
  32. * The styliser applies IRC styles to text. Styles are indicated by various control codes which are
  33. * a de-facto IRC standard.
  34. */
  35. public class Styliser implements ConfigChangeListener {
  36. /** The character used for marking up bold text. */
  37. public static final char CODE_BOLD = 2;
  38. /** The character used for marking up coloured text. */
  39. public static final char CODE_COLOUR = 3;
  40. /** The character used for marking up coloured text (using hex). */
  41. public static final char CODE_HEXCOLOUR = 4;
  42. /** Character used to indicate hyperlinks. */
  43. public static final char CODE_HYPERLINK = 5;
  44. /** Character used to indicate channel links. */
  45. public static final char CODE_CHANNEL = 6;
  46. /** Character used to indicate smilies. */
  47. public static final char CODE_SMILIE = 7;
  48. /** The character used for stopping all formatting. */
  49. public static final char CODE_STOP = 15;
  50. /** Character used to indicate nickname links. */
  51. public static final char CODE_NICKNAME = 16;
  52. /** The character used for marking up fixed pitch text. */
  53. public static final char CODE_FIXED = 17;
  54. /** The character used for negating control codes. */
  55. public static final char CODE_NEGATE = 18;
  56. /** The character used for tooltips. */
  57. public static final char CODE_TOOLTIP = 19;
  58. /** The character used for marking up italic text. */
  59. public static final char CODE_ITALIC = 29;
  60. /** The character used for marking up underlined text. */
  61. public static final char CODE_UNDERLINE = 31;
  62. /** Internal chars. */
  63. private static final String INTERNAL_CHARS = String.valueOf(CODE_HYPERLINK)
  64. + CODE_NICKNAME + CODE_CHANNEL + CODE_SMILIE + CODE_TOOLTIP;
  65. /** Characters used for hyperlinks. */
  66. private static final String HYPERLINK_CHARS = Character.toString(CODE_HYPERLINK) + CODE_CHANNEL;
  67. /** Regexp to match characters which shouldn't be used in channel links. */
  68. private static final String RESERVED_CHARS = "[^\\s" + CODE_BOLD + CODE_COLOUR
  69. + CODE_STOP + CODE_HEXCOLOUR + CODE_FIXED + CODE_ITALIC
  70. + CODE_UNDERLINE + CODE_CHANNEL + CODE_NICKNAME + CODE_NEGATE + "\",]";
  71. /** Defines all characters treated as trailing punctuation that are illegal in URLs. */
  72. private static final String URL_PUNCT_ILLEGAL = "\"";
  73. /** Defines all characters treated as trailing punctuation that're legal in URLs. */
  74. private static final String URL_PUNCT_LEGAL = "';:!,\\.\\?";
  75. /** Defines all trailing punctuation. */
  76. private static final String URL_PUNCT = URL_PUNCT_ILLEGAL + URL_PUNCT_LEGAL;
  77. /** Defines all characters allowed in URLs that aren't treated as trailing punct. */
  78. private static final String URL_NOPUNCT = "a-z0-9$\\-_@&\\+\\*\\(\\)=/#%~\\|";
  79. /** Defines all characters allowed in URLs per W3C specs. */
  80. private static final String URL_CHARS = '[' + URL_PUNCT_LEGAL + URL_NOPUNCT
  81. + "]*[" + URL_NOPUNCT + "]+[" + URL_PUNCT_LEGAL + URL_NOPUNCT + "]*";
  82. /** The regular expression to use for marking up URLs. */
  83. private static final String URL_REGEXP = "(?i)((?>(?<!" + CODE_HEXCOLOUR
  84. + "[a-f0-9]{5})[a-f]|[g-z+])+://" + URL_CHARS
  85. + "|(?<![a-z0-9:/])www\\." + URL_CHARS + ')';
  86. /** Regular expression for intelligent handling of closing brackets. */
  87. private static final String URL_INT1 = "(\\([^\\)" + HYPERLINK_CHARS
  88. + "]*(?:[" + HYPERLINK_CHARS + "][^" + HYPERLINK_CHARS + "]*["
  89. + HYPERLINK_CHARS + "])?[^\\)" + HYPERLINK_CHARS + "]*[" + HYPERLINK_CHARS
  90. + "][^" + HYPERLINK_CHARS + "]+)(\\)['\";:!,\\.\\)]*)([" + HYPERLINK_CHARS + "])";
  91. /** Regular expression for intelligent handling of trailing single and double quotes. */
  92. private static final String URL_INT2 = "(^(?:[^" + HYPERLINK_CHARS + "]+|["
  93. + HYPERLINK_CHARS + "][^" + HYPERLINK_CHARS + "][" + HYPERLINK_CHARS
  94. + "]))(['\"])([^" + HYPERLINK_CHARS + "]*?[" + HYPERLINK_CHARS + "][^"
  95. + HYPERLINK_CHARS + "]+)(\\1[" + URL_PUNCT + "]*)([" + HYPERLINK_CHARS + "])";
  96. /** Regular expression for intelligent handling of surrounding quotes. */
  97. private static final String URL_INT3 = "(['\"])([" + HYPERLINK_CHARS
  98. + "][^" + HYPERLINK_CHARS + "]+?)(\\1[^" + HYPERLINK_CHARS + "]*)(["
  99. + HYPERLINK_CHARS + "])";
  100. /** Regular expression for intelligent handling of trailing punctuation. */
  101. private static final String URL_INT4 = "([" + HYPERLINK_CHARS + "][^"
  102. + HYPERLINK_CHARS + "]+?)([" + URL_PUNCT + "]?)([" + HYPERLINK_CHARS + "])";
  103. /** The regular expression to use for marking up channels. */
  104. private static final String URL_CHANNEL = "(?i)(?<![^\\s\\+@\\-<>\\(\"',])([\\Q%s\\E]"
  105. + RESERVED_CHARS + "+)";
  106. /** Whether or not we should style links. */
  107. private boolean styleURIs;
  108. /** Whether or not we should style channel names. */
  109. private boolean styleChannels;
  110. /** Colour to use for URIs. */
  111. private Colour uriColour;
  112. /** Colour to use for channel names. */
  113. private Colour channelColour;
  114. /** Connection to get channel prefixes from, or null if not applicable. */
  115. @Nullable
  116. private final Connection connection;
  117. /** Config manager to retrieve settings from. */
  118. private final AggregateConfigProvider configManager;
  119. /** Colour manager to use to parse colours. */
  120. private final ColourManager colourManager;
  121. /**
  122. * Creates a new instance of Styliser.
  123. *
  124. * @param connection The {@link Connection} that this styliser is for. May be {@code null}.
  125. * @param configManager the {@link AggregateConfigProvider} to get settings from.
  126. * @param colourManager The {@link ColourManager} to get colours from.
  127. *
  128. * @since 0.6.3
  129. */
  130. public Styliser(@Nullable final Connection connection,
  131. final AggregateConfigProvider configManager,
  132. final ColourManager colourManager) {
  133. this.connection = connection;
  134. this.configManager = configManager;
  135. this.colourManager = colourManager;
  136. configManager.addChangeListener("ui", "linkcolour", this);
  137. configManager.addChangeListener("ui", "channelcolour", this);
  138. configManager.addChangeListener("ui", "stylelinks", this);
  139. configManager.addChangeListener("ui", "stylechannels", this);
  140. styleURIs = configManager.getOptionBool("ui", "stylelinks");
  141. styleChannels = configManager.getOptionBool("ui", "stylechannels");
  142. uriColour = colourManager.getColourFromString(
  143. configManager.getOptionString("ui", "linkcolour"), null);
  144. channelColour = colourManager.getColourFromString(
  145. configManager.getOptionString("ui", "channelcolour"), null);
  146. }
  147. /**
  148. * Stylises the specified strings and adds them to the specified maker.
  149. *
  150. * @param maker The message maker to add styling to.
  151. * @param strings The lines to be stylised
  152. */
  153. public void addStyledString(final StyledMessageMaker<?> maker, final String... strings) {
  154. maker.resetAllStyles();
  155. for (String string : strings) {
  156. final char[] chars = string.toCharArray();
  157. for (int j = 0; j < chars.length; j++) {
  158. if (chars[j] == 65533) {
  159. chars[j] = '?';
  160. }
  161. }
  162. int position = 0;
  163. final String target =
  164. doSmilies(doLinks(new String(chars).replaceAll(INTERNAL_CHARS, "")));
  165. final StyliserState state = new StyliserState();
  166. while (position < target.length()) {
  167. final String next = readUntilControl(target.substring(position));
  168. maker.appendString(next);
  169. position += next.length();
  170. if (position < target.length()) {
  171. position += readControlChars(target.substring(position), state, maker,
  172. position == 0);
  173. }
  174. }
  175. }
  176. }
  177. /**
  178. * Stylises the specified string.
  179. *
  180. * @param strings The line to be stylised
  181. *
  182. * @return StyledDocument for the inputted strings
  183. */
  184. public <T> T getStyledString(final String[] strings, final StyledMessageMaker<T> maker) {
  185. addStyledString(maker, strings);
  186. return maker.getStyledMessage();
  187. }
  188. /**
  189. * Retrieves the styled String contained within the unstyled offsets specified. That is, the
  190. * <code>from</code> and <code>to</code> arguments correspond to indexes in an unstyled version
  191. * of the <code>styled</code> string. The unstyled indices are translated to offsets within the
  192. * styled String, and the return value includes all text and control codes between those
  193. * indices.
  194. * <p>
  195. * The index translation is left-biased; that is, the indices are translated to be as far left
  196. * as they possibly can be. This means that the start of the string will include any control
  197. * codes immediately preceding the desired text, and the end will not include any trailing
  198. * codes.
  199. * <p>
  200. * This method will NOT include "internal" control codes in the output.
  201. *
  202. * @param styled The styled String to be operated on
  203. * @param from The starting index in the unstyled string
  204. * @param to The ending index in the unstyled string
  205. *
  206. * @return The corresponding text between the two indices
  207. *
  208. * @since 0.6.3
  209. */
  210. public static String getStyledText(final String styled, final int from, final int to) {
  211. checkArgument(from < to, "'from' (" + from + ") must be less than 'to' (" + to + ')');
  212. checkArgument(from >= 0, "'from' (" + from + ") must be non-negative");
  213. final String unstyled = stipControlCodes(styled);
  214. checkArgument(to <= unstyled.length(), "'to' (" + to + ") must be less than or equal to "
  215. + "the unstyled length (" + unstyled.length() + ')');
  216. final String startBit = unstyled.substring(0, from);
  217. final String middleBit = unstyled.substring(from, to);
  218. final String sanitised = stipInternalControlCodes(styled);
  219. int start = from;
  220. while (!stipControlCodes(sanitised.substring(0, start)).equals(startBit)) {
  221. start++;
  222. }
  223. int end = to + start - from;
  224. while (!stipControlCodes(sanitised.substring(start, end)).equals(middleBit)) {
  225. end++;
  226. }
  227. return sanitised.substring(start, end);
  228. }
  229. /**
  230. * Applies the hyperlink styles and intelligent linking regexps to the target.
  231. *
  232. * @param string The string to be linked
  233. *
  234. * @return A copy of the string with hyperlinks marked up
  235. */
  236. public String doLinks(final String string) {
  237. String target = string;
  238. final String prefixes = connection == null ? null
  239. : connection.getGroupChatManager().getChannelPrefixes();
  240. String target2 = target;
  241. target = target.replaceAll(URL_REGEXP, CODE_HYPERLINK + "$0" + CODE_HYPERLINK);
  242. if (prefixes != null) {
  243. target = target.replaceAll(String.format(URL_CHANNEL, prefixes),
  244. CODE_CHANNEL + "$0" + CODE_CHANNEL);
  245. }
  246. for (int j = 0; j < 5 && !target.equals(target2); j++) {
  247. target2 = target;
  248. target = target
  249. .replaceAll(URL_INT1, "$1$3$2")
  250. .replaceAll(URL_INT2, "$1$2$3$5$4")
  251. .replaceAll(URL_INT3, "$1$2$4$3")
  252. .replaceAll(URL_INT4, "$1$3$2");
  253. }
  254. return target;
  255. }
  256. /**
  257. * Applies the smilie styles to the target.
  258. *
  259. * @param string The string to be smilified
  260. *
  261. * @return A copy of the string with smilies marked up
  262. *
  263. * @since 0.6.3m1
  264. */
  265. public String doSmilies(final String string) {
  266. // TODO: Check if they're enabled.
  267. // TODO: Store the list instead of building it every line
  268. final StringBuilder smilies = new StringBuilder();
  269. configManager.getOptions("icon").entrySet().stream()
  270. .filter(icon -> icon.getKey().startsWith("smilie-")).forEach(icon -> {
  271. if (smilies.length() > 0) {
  272. smilies.append('|');
  273. }
  274. smilies.append(Pattern.quote(icon.getKey().substring(7)));
  275. });
  276. return string.replaceAll("(\\s|^)(" + smilies + ")(?=\\s|$)",
  277. "$1" + CODE_SMILIE + "$2" + CODE_SMILIE);
  278. }
  279. /**
  280. * Strips all recognised control codes from the input string.
  281. *
  282. * @param input the String to be stripped
  283. *
  284. * @return a copy of the input with control codes removed
  285. */
  286. public static String stipControlCodes(final String input) {
  287. return input.replaceAll("[" + CODE_BOLD + CODE_CHANNEL + CODE_FIXED
  288. + CODE_HYPERLINK + CODE_ITALIC + CODE_NEGATE + CODE_NICKNAME
  289. + CODE_SMILIE + CODE_STOP + CODE_UNDERLINE + "]|"
  290. + CODE_HEXCOLOUR + "([A-Za-z0-9]{6}(,[A-Za-z0-9]{6})?)?|"
  291. + CODE_COLOUR + "([0-9]{1,2}(,[0-9]{1,2})?)?", "")
  292. .replaceAll(CODE_TOOLTIP + ".*?" + CODE_TOOLTIP + "(.*?)" + CODE_TOOLTIP, "$1");
  293. }
  294. /**
  295. * St(r)ips all recognised internal control codes from the input string.
  296. *
  297. * @param input the String to be stripped
  298. *
  299. * @return a copy of the input with control codes removed
  300. *
  301. * @since 0.6.5
  302. */
  303. public static String stipInternalControlCodes(final String input) {
  304. return input.replaceAll("[" + CODE_CHANNEL + CODE_HYPERLINK + CODE_NICKNAME
  305. + CODE_SMILIE + CODE_STOP + CODE_UNDERLINE + ']', "")
  306. .replaceAll(CODE_TOOLTIP + ".*?" + CODE_TOOLTIP + "(.*?)"
  307. + CODE_TOOLTIP, "$1");
  308. }
  309. /**
  310. * Returns a substring of the input string such that no control codes are present in the output.
  311. * If the returned value isn't the same as the input, then the character immediately after is a
  312. * control character.
  313. *
  314. * @param input The string to read from
  315. *
  316. * @return A substring of the input containing no control characters
  317. */
  318. public static String readUntilControl(final String input) {
  319. int pos = input.length();
  320. pos = checkChar(pos, input.indexOf(CODE_BOLD));
  321. pos = checkChar(pos, input.indexOf(CODE_UNDERLINE));
  322. pos = checkChar(pos, input.indexOf(CODE_STOP));
  323. pos = checkChar(pos, input.indexOf(CODE_COLOUR));
  324. pos = checkChar(pos, input.indexOf(CODE_HEXCOLOUR));
  325. pos = checkChar(pos, input.indexOf(CODE_ITALIC));
  326. pos = checkChar(pos, input.indexOf(CODE_FIXED));
  327. pos = checkChar(pos, input.indexOf(CODE_HYPERLINK));
  328. pos = checkChar(pos, input.indexOf(CODE_NICKNAME));
  329. pos = checkChar(pos, input.indexOf(CODE_CHANNEL));
  330. pos = checkChar(pos, input.indexOf(CODE_SMILIE));
  331. pos = checkChar(pos, input.indexOf(CODE_NEGATE));
  332. pos = checkChar(pos, input.indexOf(CODE_TOOLTIP));
  333. return input.substring(0, pos);
  334. }
  335. /**
  336. * Helper function used in readUntilControl. Checks if i is a valid index of the string (i.e.,
  337. * it's not -1), and then returns the minimum of pos and i.
  338. *
  339. * @param pos The current position in the string
  340. * @param i The index of the first occurrence of some character
  341. *
  342. * @return The new position (see implementation)
  343. */
  344. private static int checkChar(final int pos, final int i) {
  345. if (i < pos && i != -1) {
  346. return i;
  347. }
  348. return pos;
  349. }
  350. /**
  351. * Reads the first control character from the input string (and any arguments it takes), and
  352. * applies it to the specified attribute set.
  353. *
  354. * @return The number of characters read as control characters
  355. *@param string The string to read from
  356. * @param maker The attribute set that new attributes will be applied to
  357. * @param isStart Whether this is at the start of the string or not
  358. */
  359. private int readControlChars(final String string,
  360. final StyliserState state,
  361. final StyledMessageMaker<?> maker, final boolean isStart) {
  362. final boolean isNegated = state.isNegated;
  363. // Bold
  364. if (string.charAt(0) == CODE_BOLD) {
  365. if (!isNegated) {
  366. maker.toggleBold();
  367. }
  368. return 1;
  369. }
  370. // Underline
  371. if (string.charAt(0) == CODE_UNDERLINE) {
  372. if (!isNegated) {
  373. maker.toggleUnderline();
  374. }
  375. return 1;
  376. }
  377. // Italic
  378. if (string.charAt(0) == CODE_ITALIC) {
  379. if (!isNegated) {
  380. maker.toggleItalic();
  381. }
  382. return 1;
  383. }
  384. // Hyperlinks
  385. if (string.charAt(0) == CODE_HYPERLINK) {
  386. if (!isNegated && styleURIs) {
  387. maker.toggleHyperlinkStyle(uriColour);
  388. }
  389. if (state.isInLink) {
  390. maker.endHyperlink();
  391. } else {
  392. maker.startHyperlink(readUntilControl(string.substring(1)));
  393. }
  394. state.isInLink = !state.isInLink;
  395. return 1;
  396. }
  397. // Channel links
  398. if (string.charAt(0) == CODE_CHANNEL) {
  399. if (!isNegated && styleChannels) {
  400. maker.toggleChannelLinkStyle(channelColour);
  401. }
  402. if (state.isInLink) {
  403. maker.endChannelLink();
  404. } else {
  405. maker.startChannelLink(readUntilControl(string.substring(1)));
  406. }
  407. state.isInLink = !state.isInLink;
  408. return 1;
  409. }
  410. // Nickname links
  411. if (string.charAt(0) == CODE_NICKNAME) {
  412. if (state.isInLink) {
  413. maker.endNicknameLink();
  414. } else {
  415. maker.startNicknameLink(readUntilControl(string.substring(1)));
  416. }
  417. state.isInLink = !state.isInLink;
  418. return 1;
  419. }
  420. // Fixed pitch
  421. if (string.charAt(0) == CODE_FIXED) {
  422. if (!isNegated) {
  423. maker.toggleFixedWidth();
  424. }
  425. return 1;
  426. }
  427. // Stop formatting
  428. if (string.charAt(0) == CODE_STOP) {
  429. if (!isNegated) {
  430. maker.resetAllStyles();
  431. }
  432. return 1;
  433. }
  434. // Colours
  435. if (string.charAt(0) == CODE_COLOUR) {
  436. int count = 1;
  437. // This isn't too nice!
  438. if (string.length() > count && isInt(string.charAt(count))) {
  439. int foreground = string.charAt(count) - '0';
  440. count++;
  441. if (string.length() > count && isInt(string.charAt(count))) {
  442. foreground = foreground * 10 + string.charAt(count) - '0';
  443. count++;
  444. }
  445. foreground %= 16;
  446. if (!isNegated) {
  447. maker.setForeground(colourManager.getColourFromString(
  448. String.valueOf(foreground), Colour.WHITE));
  449. if (isStart) {
  450. maker.setDefaultForeground(colourManager
  451. .getColourFromString(String.valueOf(foreground), Colour.WHITE));
  452. }
  453. }
  454. // Now background
  455. if (string.length() > count && string.charAt(count) == ','
  456. && string.length() > count + 1
  457. && isInt(string.charAt(count + 1))) {
  458. int background = string.charAt(count + 1) - '0';
  459. count += 2; // Comma and first digit
  460. if (string.length() > count && isInt(string.charAt(count))) {
  461. background = background * 10 + string.charAt(count) - '0';
  462. count++;
  463. }
  464. background %= 16;
  465. if (!isNegated) {
  466. maker.setBackground(colourManager
  467. .getColourFromString(String.valueOf(background), Colour.WHITE));
  468. if (isStart) {
  469. maker.setDefaultBackground(colourManager
  470. .getColourFromString(String.valueOf(background), Colour.WHITE));
  471. }
  472. }
  473. }
  474. } else if (!isNegated) {
  475. maker.resetColours();
  476. }
  477. return count;
  478. }
  479. // Hex colours
  480. if (string.charAt(0) == CODE_HEXCOLOUR) {
  481. int count = 1;
  482. if (hasHexString(string, 1)) {
  483. if (!isNegated) {
  484. maker.setForeground(
  485. colourManager.getColourFromString(string.substring(1, 7).toUpperCase(),
  486. Colour.WHITE));
  487. if (isStart) {
  488. maker.setDefaultForeground(
  489. colourManager.getColourFromString(
  490. string.substring(1, 7).toUpperCase(), Colour.WHITE));
  491. }
  492. }
  493. count += 6;
  494. if (string.length() == count) {
  495. return count;
  496. }
  497. // Now for background
  498. if (string.charAt(count) == ',' && hasHexString(string, count + 1)) {
  499. count++;
  500. if (!isNegated) {
  501. maker.setBackground(colourManager.getColourFromString(
  502. string.substring(count, count + 6).toUpperCase(), Colour.WHITE));
  503. if (isStart) {
  504. maker.setDefaultBackground(colourManager.getColourFromString(
  505. string.substring(count, count + 6).toUpperCase(), Colour.WHITE));
  506. }
  507. }
  508. count += 6;
  509. }
  510. } else if (!isNegated) {
  511. maker.resetColours();
  512. }
  513. return count;
  514. }
  515. // Control code negation
  516. if (string.charAt(0) == CODE_NEGATE) {
  517. state.isNegated = !state.isNegated;
  518. return 1;
  519. }
  520. // Smilies!!
  521. if (string.charAt(0) == CODE_SMILIE) {
  522. if (state.isInSmilie) {
  523. maker.endSmilie();
  524. } else {
  525. maker.startSmilie("smilie-" + readUntilControl(string.substring(1)));
  526. }
  527. state.isInSmilie = !state.isInSmilie;
  528. return 1;
  529. }
  530. // Tooltips
  531. if (string.charAt(0) == CODE_TOOLTIP) {
  532. if (state.isInToolTip) {
  533. maker.endToolTip();
  534. } else {
  535. final int index = string.indexOf(CODE_TOOLTIP, 1);
  536. if (index == -1) {
  537. // Doesn't make much sense, let's ignore it!
  538. return 1;
  539. }
  540. final String tooltip = string.substring(1, index);
  541. maker.startToolTip(tooltip);
  542. state.isInToolTip = !state.isInToolTip;
  543. return tooltip.length() + 2;
  544. }
  545. state.isInToolTip = !state.isInToolTip;
  546. return 1;
  547. }
  548. return 0;
  549. }
  550. /**
  551. * Determines if the specified character represents a single integer (i.e. 0-9).
  552. *
  553. * @param c The character to check
  554. *
  555. * @return True iff the character is in the range [0-9], false otherwise
  556. */
  557. private static boolean isInt(final char c) {
  558. return c >= '0' && c <= '9';
  559. }
  560. /**
  561. * Determines if the specified character represents a single hex digit (i.e., 0-F).
  562. *
  563. * @param c The character to check
  564. *
  565. * @return True iff the character is in the range [0-F], false otherwise
  566. */
  567. private static boolean isHex(final char c) {
  568. return isInt(c) || c >= 'A' && c <= 'F';
  569. }
  570. /**
  571. * Determines if the specified string has a 6-digit hex string starting at the specified offset.
  572. *
  573. * @param input The string to check
  574. * @param offset The offset to start at
  575. *
  576. * @return True iff there is a hex string preset at the offset
  577. */
  578. private static boolean hasHexString(final String input, final int offset) {
  579. // If the string's too short, it can't have a hex string
  580. if (input.length() < offset + 6) {
  581. return false;
  582. }
  583. boolean res = true;
  584. for (int i = offset; i < 6 + offset; i++) {
  585. res = res && isHex(input.toUpperCase(Locale.getDefault()).charAt(i));
  586. }
  587. return res;
  588. }
  589. @Override
  590. public void configChanged(final String domain, final String key) {
  591. switch (key) {
  592. case "stylelinks":
  593. styleURIs = configManager.getOptionBool("ui", "stylelinks");
  594. break;
  595. case "stylechannels":
  596. styleChannels = configManager.getOptionBool("ui", "stylechannels");
  597. break;
  598. case "linkcolour":
  599. uriColour = colourManager.getColourFromString(
  600. configManager.getOptionString("ui", "linkcolour"), null);
  601. break;
  602. case "channelcolour":
  603. channelColour = colourManager.getColourFromString(
  604. configManager.getOptionString("ui", "channelcolour"), null);
  605. break;
  606. }
  607. }
  608. private static class StyliserState {
  609. public boolean isNegated;
  610. public boolean isInLink;
  611. public boolean isInSmilie;
  612. public boolean isInToolTip;
  613. }
  614. }