| Author | DaveJarvis <email> |
|---|---|
| Date | 2021-12-05 11:52:46 GMT-0800 |
| Commit | 079c4fc7d6a7171583974b6859f035f7ed0fc216 |
| Parent | 3ead2c2 |
| Delta | 127 lines added, 30 lines removed, 97-line increase |
| +package com.keenwrite.util; | ||
| + | ||
| +import com.keenwrite.sigils.SigilOperator; | ||
| +import com.keenwrite.sigils.Sigils; | ||
| + | ||
| +import java.util.HashMap; | ||
| +import java.util.Map; | ||
| +import java.util.concurrent.ConcurrentHashMap; | ||
| +import java.util.regex.Pattern; | ||
| + | ||
| +import static java.lang.String.format; | ||
| +import static java.util.regex.Pattern.compile; | ||
| +import static java.util.regex.Pattern.quote; | ||
| + | ||
| +public class InterpolatingMap extends ConcurrentHashMap<String, String> { | ||
| + private static final int GROUP_DELIMITED = 1; | ||
| + | ||
| + /** | ||
| + * Used to override the default initial capacity in {@link HashMap}. | ||
| + */ | ||
| + private static final int INITIAL_CAPACITY = 1 << 8; | ||
| + | ||
| + public InterpolatingMap() { | ||
| + super( INITIAL_CAPACITY ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Interpolates all values in the map that reference other values by way | ||
| + * of key names. Performs a non-greedy match of key names delimited by | ||
| + * definition tokens. This operation modifies the map directly. | ||
| + * | ||
| + * @param operator Contains the opening and closing sigils that mark | ||
| + * where variable names begin and end. | ||
| + * @return {@code this} | ||
| + */ | ||
| + public Map<String, String> interpolate( final SigilOperator operator ) { | ||
| + sigilize( operator ); | ||
| + interpolate( operator.getSigils() ); | ||
| + return this; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Wraps each key in this map with the starting and ending sigils provided | ||
| + * by the given {@link SigilOperator}. This operation modifies the map | ||
| + * directly. | ||
| + * | ||
| + * @param operator Container for starting and ending sigils. | ||
| + */ | ||
| + private void sigilize( final SigilOperator operator ) { | ||
| + forEach( ( k, v ) -> put( operator.entoken( k ), v ) ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Interpolates all values in the map that reference other values by way | ||
| + * of key names. Performs a non-greedy match of key names delimited by | ||
| + * definition tokens. This operation modifies the map directly. | ||
| + * | ||
| + * @param sigils Contains the opening and closing sigils that mark | ||
| + * where variable names begin and end. | ||
| + */ | ||
| + private void interpolate( final Sigils sigils ) { | ||
| + final var pattern = compile( | ||
| + format( | ||
| + "(%s.*?%s)", quote( sigils.getBegan() ), quote( sigils.getEnded() ) | ||
| + ) | ||
| + ); | ||
| + | ||
| + replaceAll( ( k, v ) -> resolve( v, pattern ) ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Given a value with zero or more key references, this will resolve all | ||
| + * the values, recursively. If a key cannot be de-referenced, the value will | ||
| + * contain the key name. | ||
| + * | ||
| + * @param value Value containing zero or more key references. | ||
| + * @param pattern The regular expression pattern to match variable key names. | ||
| + * @return The given value with all embedded key references interpolated. | ||
| + */ | ||
| + private String resolve( String value, final Pattern pattern ) { | ||
| + final var matcher = pattern.matcher( value ); | ||
| + | ||
| + while( matcher.find() ) { | ||
| + final var keyName = matcher.group( GROUP_DELIMITED ); | ||
| + final var mapValue = get( keyName ); | ||
| + final var keyValue = mapValue == null | ||
| + ? keyName | ||
| + : resolve( mapValue, pattern ); | ||
| + | ||
| + value = value.replace( keyName, keyValue ); | ||
| + } | ||
| + | ||
| + return value; | ||
| + } | ||
| +} | ||
| } | ||
| + public Sigils getSigils() { | ||
| + return mSigils; | ||
| + } | ||
| + | ||
| /** | ||
| * Wraps the given key in the began and ended tokens. This may perform any |
| */ | ||
| public final class YamlSigilOperator extends SigilOperator { | ||
| - public static final char KEY_SEPARATOR_DEF = '.'; | ||
| - | ||
| public YamlSigilOperator( final Sigils sigils ) { | ||
| super( sigils ); |
| */ | ||
| public class ExportFailedEvent implements AppEvent { | ||
| - public static void fireExportFailedEvent() { | ||
| - new ExportFailedEvent().fire(); | ||
| + public static void fire() { | ||
| + new ExportFailedEvent().publish(); | ||
| } | ||
| } |
| * @param uri The instance of {@link URI} to open as a file in a text editor. | ||
| */ | ||
| - public static void fireFileOpenEvent( final URI uri ) { | ||
| - new FileOpenEvent( uri ).fire(); | ||
| + public static void fire( final URI uri ) { | ||
| + new FileOpenEvent( uri ).publish(); | ||
| } | ||
| * @param uri The location to open. | ||
| */ | ||
| - public static void fireHyperlinkOpenEvent( final URI uri ) | ||
| + public static void fire( final URI uri ) | ||
| throws IOException { | ||
| - new HyperlinkOpenEvent( uri ).fire(); | ||
| + new HyperlinkOpenEvent( uri ).publish(); | ||
| } | ||
| /** | ||
| * Requests to open the default browser at the given location. | ||
| * | ||
| * @param uri The location to open. | ||
| */ | ||
| - public static void fireHyperlinkOpenEvent( final String uri ) { | ||
| + public static void fire( final String uri ) { | ||
| try { | ||
| - fireHyperlinkOpenEvent( new URI( uri ) ); | ||
| + fire( new URI( uri ) ); | ||
| } catch( final Exception ex ) { | ||
| clue( ex ); |
| */ | ||
| public static void fireNewOutlineEvent() { | ||
| - new ParseHeadingEvent( NEW_OUTLINE_LEVEL, "Document", 0 ).fire(); | ||
| + new ParseHeadingEvent( NEW_OUTLINE_LEVEL, "Document", 0 ).publish(); | ||
| } | ||
| /** | ||
| * Call to indicate that a new heading must be added to the document outline. | ||
| * | ||
| * @param text The heading text (parsed and processed). | ||
| * @param level A value between 1 and 6. | ||
| * @param offset Absolute offset into document where heading is found. | ||
| */ | ||
| - public static void fireNewHeadingEvent( | ||
| + public static void fire( | ||
| final int level, final String text, final int offset ) { | ||
| assert text != null; | ||
| assert 1 <= level && level <= 6; | ||
| assert 0 <= offset; | ||
| - new ParseHeadingEvent( level, text, offset ).fire(); | ||
| + new ParseHeadingEvent( level, text, offset ).publish(); | ||
| } | ||
| private static void fire( final boolean locked ) { | ||
| - new ScrollLockEvent( locked ).fire(); | ||
| + new ScrollLockEvent( locked ).publish(); | ||
| } | ||
| */ | ||
| public static void clue() { | ||
| - fireStatusEvent( get( STATUS_BAR_OK, "OK" ) ); | ||
| + fire( get( STATUS_BAR_OK, "OK" ) ); | ||
| } | ||
| /** | ||
| * Notifies listeners of a series of messages. This is useful when providing | ||
| * users feedback of how third-party executables have failed. | ||
| * | ||
| * @param messages The lines of text to display. | ||
| */ | ||
| public static void clue( final List<String> messages ) { | ||
| - messages.forEach( StatusEvent::fireStatusEvent ); | ||
| + messages.forEach( StatusEvent::fire ); | ||
| } | ||
| /** | ||
| * Notifies listeners of an error. | ||
| * | ||
| * @param key The message bundle key to look up. | ||
| * @param t The exception that caused the error. | ||
| */ | ||
| public static void clue( final String key, final Throwable t ) { | ||
| - fireStatusEvent( get( key ), t ); | ||
| + fire( get( key ), t ); | ||
| } | ||
| /** | ||
| * Notifies listeners of a custom message. | ||
| * | ||
| * @param key The property key having a value to populate with arguments. | ||
| * @param args The placeholder values to substitute into the key's value. | ||
| */ | ||
| public static void clue( final String key, final Object... args ) { | ||
| - fireStatusEvent( get( key, args ) ); | ||
| + fire( get( key, args ) ); | ||
| } | ||
| /** | ||
| * Notifies listeners of an exception occurs that warrants the user's | ||
| * attention. | ||
| * | ||
| * @param problem The exception with a message to display to the user. | ||
| */ | ||
| public static void clue( final Throwable problem ) { | ||
| - fireStatusEvent( problem ); | ||
| + fire( problem ); | ||
| } | ||
| - private static void fireStatusEvent( final String message ) { | ||
| - new StatusEvent( message ).fire(); | ||
| + private static void fire( final String message ) { | ||
| + new StatusEvent( message ).publish(); | ||
| } | ||
| - private static void fireStatusEvent( final Throwable problem ) { | ||
| - new StatusEvent( problem ).fire(); | ||
| + private static void fire( final Throwable problem ) { | ||
| + new StatusEvent( problem ).publish(); | ||
| } | ||
| - private static void fireStatusEvent( | ||
| + private static void fire( | ||
| final String message, final Throwable problem ) { | ||
| - new StatusEvent( message, problem ).fire(); | ||
| + new StatusEvent( message, problem ).publish(); | ||
| } | ||
| } |
| * @param editor The instance of editor that has gained input focus. | ||
| */ | ||
| - public static void fireTextDefinitionFocus( final TextDefinition editor ) { | ||
| - new TextDefinitionFocusEvent( editor ).fire(); | ||
| + public static void fire( final TextDefinition editor ) { | ||
| + new TextDefinitionFocusEvent( editor ).publish(); | ||
| } | ||
| } |
| */ | ||
| public static void fireTextEditorFocus( final TextEditor editor ) { | ||
| - new TextEditorFocusEvent( editor ).fire(); | ||
| + new TextEditorFocusEvent( editor ).publish(); | ||
| } | ||
| } |
| * @param count The approximate number of words in the document. | ||
| */ | ||
| - public static void fireWordCountEvent( final int count ) { | ||
| - new WordCountEvent( count ).fire(); | ||
| + public static void fire( final int count ) { | ||
| + new WordCountEvent( count ).publish(); | ||
| } | ||