Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
keenwrite.sh
#!/usr/bin/env bash
+SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)
+
java \
-Dprism.order=sw \
--add-exports=javafx.graphics/com.sun.javafx.scene.text=ALL-UNNAMED \
--add-exports=javafx.graphics/com.sun.javafx.scene.traversal=ALL-UNNAMED \
- -jar keenwrite.jar $@
+ -jar ${SCRIPT_DIR}/keenwrite.jar $@
src/main/java/com/keenwrite/MainApp.java
import static com.keenwrite.preferences.AppKeys.*;
import static com.keenwrite.util.FontLoader.initFonts;
-import static javafx.scene.input.KeyCode.ESCAPE;
import static javafx.scene.input.KeyCode.F11;
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
private Workspace mWorkspace;
- private MainScene mMainScene;
/**
// instance will be loaded and applied.
final var property = mWorkspace.localeProperty( KEY_LANGUAGE_LOCALE );
- property.addListener( ( c, o, n ) -> readLexicon() );
+ property.addListener( ( _, _, _ ) -> readLexicon() );
initFonts();
// Load the lexicon and check all the documents after all files are open.
- stage.addEventFilter( WINDOW_SHOWN, event -> readLexicon() );
+ stage.addEventFilter( WINDOW_SHOWN, _ -> readLexicon() );
stage.show();
if( F11.equals( event.getCode() ) ) {
stage.setFullScreen( !stage.isFullScreen() );
- }
- } );
-
- // After the app loses focus, when the user switches back using Alt+Tab,
- // the menu is engaged on Windows. Simulate an ESC keypress to the menu
- // to disable the menu, giving focus back to the application proper.
- //
- // JavaFX Bug: https://bugs.openjdk.java.net/browse/JDK-8090647
- stage.focusedProperty().addListener( ( c, lost, found ) -> {
- if( found ) {
- mMainScene.getMenuBar().fireEvent( keyDown( ESCAPE ) );
}
} );
}
private void initIcons( final Stage stage ) {
stage.getIcons().addAll( LOGOS );
}
private void initScene( final Stage stage ) {
- mMainScene = new MainScene( mWorkspace );
- stage.setScene( mMainScene.getScene() );
+ final var mainScene = new MainScene( mWorkspace );
+ stage.setScene( mainScene.getScene() );
}
src/main/java/com/keenwrite/MainPane.java
.with( Mutator::setFontDir,
() -> w.getFile( KEY_TYPESET_CONTEXT_FONTS_DIR ) )
- .with( Mutator::setEnableMode,
+ .with( Mutator::setModesEnabled,
() -> w.getString( KEY_TYPESET_MODES_ENABLED ) )
.with( Mutator::setCurlQuotes,
src/main/java/com/keenwrite/cmdline/Arguments.java
paramLabel = "String"
)
- private String mEnableMode;
+ private String mTypesetMode;
@CommandLine.Option(
.with( Mutator::setImageOrder, () -> mImageOrder )
.with( Mutator::setFontDir, () -> mFontDir )
- .with( Mutator::setEnableMode, () -> mEnableMode )
+ .with( Mutator::setModesEnabled, () -> mTypesetMode )
.with( Mutator::setExportFormat, format )
.with( Mutator::setDefinitions, () -> definitions )
src/main/java/com/keenwrite/collections/InterpolatingMap.java
if( mapValue != null ) {
+ if( mapValue.contains( mOperator.apply( keyName ) ) ) {
+ throw new IllegalStateException(
+ STR."Mapped value '\{mapValue}' may not contain its key name '\{keyName}'" );
+ }
+
final var keyValue = interpolate( mapValue );
matcher.appendReplacement( sb, quoteReplacement( keyValue ) );
src/main/java/com/keenwrite/events/StatusEvent.java
}
- private static void fire( final String message ) {
+ public static void fire( final String message ) {
new StatusEvent( message ).publish();
}
src/main/java/com/keenwrite/processors/PdfProcessor.java
clue( "Main.status.typeset.setting", "r-work", rWorkDir );
- final var enableMode = sanitize( context.getEnableMode() );
- clue( "Main.status.typeset.setting", "mode", enableMode );
+ final var modesEnabled = sanitize( context.getModesEnabled() );
+ clue( "Main.status.typeset.setting", "mode", modesEnabled );
final var autoRemove = context.getAutoRemove();
.with( Mutator::setCacheDir, cacheDir )
.with( Mutator::setFontDir, fontDir )
- .with( Mutator::setEnableMode, enableMode )
+ .with( Mutator::setModesEnabled, modesEnabled )
.with( Mutator::setAutoRemove, autoRemove )
.build();
src/main/java/com/keenwrite/processors/ProcessorContext.java
private Supplier<Path> mFontDir = () -> getFontDirectory().toPath();
- private Supplier<String> mEnableMode = () -> "";
-
- private Supplier<String> mSigilBegan = () -> DEF_DELIM_BEGAN_DEFAULT;
- private Supplier<String> mSigilEnded = () -> DEF_DELIM_ENDED_DEFAULT;
-
- private Supplier<Path> mRWorkingDir = USER_DIRECTORY::toPath;
- private Supplier<String> mRScript = () -> "";
-
- private Supplier<Boolean> mCurlQuotes = () -> true;
- private Supplier<Boolean> mAutoRemove = () -> true;
-
- public void setSourcePath( final Path sourcePath ) {
- assert sourcePath != null;
- mSourcePath = sourcePath;
- }
-
- public void setTargetPath( final Path outputPath ) {
- assert outputPath != null;
- mTargetPath = outputPath;
- }
-
- public void setThemeDir( final Supplier<Path> themeDir ) {
- assert themeDir != null;
- mThemeDir = themeDir;
- }
-
- public void setCacheDir( final Supplier<File> cacheDir ) {
- assert cacheDir != null;
-
- mCacheDir = () -> {
- final var dir = cacheDir.get();
-
- return (dir == null ? toFile( USER_DATA_DIR ) : dir).toPath();
- };
- }
-
- public void setImageDir( final Supplier<File> imageDir ) {
- assert imageDir != null;
-
- mImageDir = () -> {
- final var dir = imageDir.get();
-
- return (dir == null ? USER_DIRECTORY : dir).toPath();
- };
- }
-
- public void setImageOrder( final Supplier<String> imageOrder ) {
- assert imageOrder != null;
- mImageOrder = imageOrder;
- }
-
- public void setImageServer( final Supplier<String> imageServer ) {
- assert imageServer != null;
- mImageServer = imageServer;
- }
-
- public void setFontDir( final Supplier<File> fontDir ) {
- assert fontDir != null;
-
- mFontDir = () -> {
- final var dir = fontDir.get();
-
- return (dir == null ? USER_DIRECTORY : dir).toPath();
- };
- }
-
- public void setEnableMode( final Supplier<String> enableMode ) {
- assert enableMode != null;
- mEnableMode = enableMode;
- }
-
- public void setExportFormat( final ExportFormat exportFormat ) {
- assert exportFormat != null;
- mExportFormat = exportFormat;
- }
-
- public void setConcatenate( final Supplier<Boolean> concatenate ) {
- mConcatenate = concatenate;
- }
-
- public void setChapters( final Supplier<String> chapters ) {
- mChapters = chapters;
- }
-
- public void setLocale( final Supplier<Locale> locale ) {
- assert locale != null;
- mLocale = locale;
- }
-
- /**
- * Sets the list of fully interpolated key-value pairs to use when
- * substituting variable names back into the document as variable values.
- * This uses a {@link Callable} reference so that GUI and command-line
- * usage can insert their respective behaviours. That is, this method
- * prevents coupling the GUI to the CLI.
- *
- * @param supplier Defines how to retrieve the definitions.
- */
- public void setDefinitions( final Supplier<Map<String, String>> supplier ) {
- assert supplier != null;
- mDefinitions = supplier;
- }
-
- /**
- * Sets metadata to use in the document header. These are made available
- * to the typesetting engine as {@code \documentvariable} values.
- *
- * @param metadata The key/value pairs to publish as document metadata.
- */
- public void setMetadata( final Supplier<Map<String, String>> metadata ) {
- assert metadata != null;
- mMetadata = metadata.get() == null ? HashMap::new : metadata;
- }
-
- /**
- * Sets document variables to use when building the document. These
- * variables will override existing key/value pairs, or be added as
- * new key/value pairs if not already defined. This allows users to
- * inject variables into the document from the command-line, allowing
- * for dynamic assignment of in-text values when building documents.
- *
- * @param overrides The key/value pairs to add (or override) as variables.
- */
- public void setOverrides( final Supplier<Map<String, String>> overrides ) {
- assert overrides != null;
- assert mDefinitions != null;
- assert mDefinitions.get() != null;
-
- final var map = overrides.get();
-
- if( map != null ) {
- mDefinitions.get().putAll( map );
- }
- }
-
- /**
- * Sets the source for deriving the {@link Caret}. Typically, this is
- * the text editor that has focus.
- *
- * @param caret The source for the currently active caret.
- */
- public void setCaret( final Supplier<Caret> caret ) {
- assert caret != null;
- mCaret = caret;
- }
-
- public void setSigilBegan( final Supplier<String> sigilBegan ) {
- assert sigilBegan != null;
- mSigilBegan = sigilBegan;
- }
-
- public void setSigilEnded( final Supplier<String> sigilEnded ) {
- assert sigilEnded != null;
- mSigilEnded = sigilEnded;
- }
-
- public void setRWorkingDir( final Supplier<Path> rWorkingDir ) {
- assert rWorkingDir != null;
- mRWorkingDir = rWorkingDir;
- }
-
- public void setRScript( final Supplier<String> rScript ) {
- assert rScript != null;
- mRScript = rScript;
- }
-
- public void setCurlQuotes( final Supplier<Boolean> curlQuotes ) {
- assert curlQuotes != null;
- mCurlQuotes = curlQuotes;
- }
-
- public void setAutoRemove( final Supplier<Boolean> autoRemove ) {
- assert autoRemove != null;
- mAutoRemove = autoRemove;
- }
-
- private boolean isExportFormat( final ExportFormat format ) {
- return mExportFormat == format;
- }
- }
-
- public static GenericBuilder<Mutator, ProcessorContext> builder() {
- return GenericBuilder.of( Mutator::new, ProcessorContext::new );
- }
-
- /**
- * Creates a new context for use by the {@link ProcessorFactory} when
- * instantiating new {@link Processor} instances. Although all the
- * parameters are required, not all {@link Processor} instances will use
- * all parameters.
- */
- private ProcessorContext( final Mutator mutator ) {
- assert mutator != null;
-
- mMutator = mutator;
- }
-
- public Path getSourcePath() {
- return mMutator.mSourcePath;
- }
-
- /**
- * Answers what type of input document is to be processed.
- *
- * @return The input document's {@link MediaType}.
- */
- public MediaType getSourceType() {
- return MediaTypeExtension.fromPath( mMutator.mSourcePath );
- }
-
- /**
- * Fully qualified file name to use when exporting (e.g., document.pdf).
- *
- * @return Full path to a file name.
- */
- public Path getTargetPath() {
- return mMutator.mTargetPath;
- }
-
- public ExportFormat getExportFormat() {
- return mMutator.mExportFormat;
- }
-
- public Locale getLocale() {
- return mMutator.mLocale.get();
- }
-
- /**
- * Returns the variable map of definitions, without interpolation.
- *
- * @return A map to help dereference variables.
- */
- public Map<String, String> getDefinitions() {
- return mMutator.mDefinitions.get();
- }
-
- /**
- * Returns the variable map of definitions, with interpolation.
- *
- * @return A map to help dereference variables.
- */
- public InterpolatingMap getInterpolatedDefinitions() {
- return new InterpolatingMap(
- createDefinitionKeyOperator(), getDefinitions()
- ).interpolate();
- }
-
- public Map<String, String> getMetadata() {
- return mMutator.mMetadata.get();
- }
-
- /**
- * Returns the current caret position in the document being edited and is
- * always up-to-date.
- *
- * @return Caret position in the document.
- */
- public Supplier<Caret> getCaret() {
- return mMutator.mCaret;
- }
-
- /**
- * Returns the directory that contains the file being edited. When
- * {@link Constants#DOCUMENT_DEFAULT} is created, the parent path is
- * {@code null}. This will get absolute path to the file before trying to
- * get te parent path, which should always be a valid path. In the unlikely
- * event that the base path cannot be determined by the path alone, the
- * default user directory is returned. This is necessary for the creation
- * of new files.
- *
- * @return Path to the directory containing a file being edited, or the
- * default user directory if the base path cannot be determined.
- */
- public Path getBaseDir() {
- final var path = getSourcePath().toAbsolutePath().getParent();
- return path == null ? DEFAULT_DIRECTORY : path;
- }
-
- FileType getSourceFileType() {
- return lookup( getSourcePath() );
- }
-
- public Path getThemeDir() {
- return mMutator.mThemeDir.get();
- }
-
- public Path getImageDir() {
- return mMutator.mImageDir.get();
- }
-
- public Path getCacheDir() {
- return mMutator.mCacheDir.get();
- }
-
- public Iterable<String> getImageOrder() {
- assert mMutator.mImageOrder != null;
-
- final var order = mMutator.mImageOrder.get();
- final var token = order.contains( "," ) ? ',' : ' ';
-
- return Splitter.on( token ).split( token + order );
- }
-
- public String getImageServer() {
- return mMutator.mImageServer.get();
- }
-
- public Path getFontDir() {
- return mMutator.mFontDir.get();
- }
-
- public String getEnableMode() {
- final var processor = new VariableProcessor( IDENTITY, this );
- final var needles = processor.getDefinitions();
- final var haystack = mMutator.mEnableMode.get();
- final var result = replace( haystack, needles );
-
- // If no replacement was made, then the mode variable isn't set.
- return result.equals( haystack ) ? "" : result;
+ private Supplier<String> mModesEnabled = () -> "";
+
+ private Supplier<String> mSigilBegan = () -> DEF_DELIM_BEGAN_DEFAULT;
+ private Supplier<String> mSigilEnded = () -> DEF_DELIM_ENDED_DEFAULT;
+
+ private Supplier<Path> mRWorkingDir = USER_DIRECTORY::toPath;
+ private Supplier<String> mRScript = () -> "";
+
+ private Supplier<Boolean> mCurlQuotes = () -> true;
+ private Supplier<Boolean> mAutoRemove = () -> true;
+
+ public void setSourcePath( final Path sourcePath ) {
+ assert sourcePath != null;
+ mSourcePath = sourcePath;
+ }
+
+ public void setTargetPath( final Path outputPath ) {
+ assert outputPath != null;
+ mTargetPath = outputPath;
+ }
+
+ public void setThemeDir( final Supplier<Path> themeDir ) {
+ assert themeDir != null;
+ mThemeDir = themeDir;
+ }
+
+ public void setCacheDir( final Supplier<File> cacheDir ) {
+ assert cacheDir != null;
+
+ mCacheDir = () -> {
+ final var dir = cacheDir.get();
+
+ return (dir == null ? toFile( USER_DATA_DIR ) : dir).toPath();
+ };
+ }
+
+ public void setImageDir( final Supplier<File> imageDir ) {
+ assert imageDir != null;
+
+ mImageDir = () -> {
+ final var dir = imageDir.get();
+
+ return (dir == null ? USER_DIRECTORY : dir).toPath();
+ };
+ }
+
+ public void setImageOrder( final Supplier<String> imageOrder ) {
+ assert imageOrder != null;
+ mImageOrder = imageOrder;
+ }
+
+ public void setImageServer( final Supplier<String> imageServer ) {
+ assert imageServer != null;
+ mImageServer = imageServer;
+ }
+
+ public void setFontDir( final Supplier<File> fontDir ) {
+ assert fontDir != null;
+
+ mFontDir = () -> {
+ final var dir = fontDir.get();
+
+ return (dir == null ? USER_DIRECTORY : dir).toPath();
+ };
+ }
+
+ public void setModesEnabled( final Supplier<String> modesEnabled ) {
+ assert modesEnabled != null;
+ mModesEnabled = modesEnabled;
+ }
+
+ public void setExportFormat( final ExportFormat exportFormat ) {
+ assert exportFormat != null;
+ mExportFormat = exportFormat;
+ }
+
+ public void setConcatenate( final Supplier<Boolean> concatenate ) {
+ mConcatenate = concatenate;
+ }
+
+ public void setChapters( final Supplier<String> chapters ) {
+ mChapters = chapters;
+ }
+
+ public void setLocale( final Supplier<Locale> locale ) {
+ assert locale != null;
+ mLocale = locale;
+ }
+
+ /**
+ * Sets the list of fully interpolated key-value pairs to use when
+ * substituting variable names back into the document as variable values.
+ * This uses a {@link Callable} reference so that GUI and command-line
+ * usage can insert their respective behaviours. That is, this method
+ * prevents coupling the GUI to the CLI.
+ *
+ * @param supplier Defines how to retrieve the definitions.
+ */
+ public void setDefinitions( final Supplier<Map<String, String>> supplier ) {
+ assert supplier != null;
+ mDefinitions = supplier;
+ }
+
+ /**
+ * Sets metadata to use in the document header. These are made available
+ * to the typesetting engine as {@code \documentvariable} values.
+ *
+ * @param metadata The key/value pairs to publish as document metadata.
+ */
+ public void setMetadata( final Supplier<Map<String, String>> metadata ) {
+ assert metadata != null;
+ mMetadata = metadata.get() == null ? HashMap::new : metadata;
+ }
+
+ /**
+ * Sets document variables to use when building the document. These
+ * variables will override existing key/value pairs, or be added as
+ * new key/value pairs if not already defined. This allows users to
+ * inject variables into the document from the command-line, allowing
+ * for dynamic assignment of in-text values when building documents.
+ *
+ * @param overrides The key/value pairs to add (or override) as variables.
+ */
+ public void setOverrides( final Supplier<Map<String, String>> overrides ) {
+ assert overrides != null;
+ assert mDefinitions != null;
+ assert mDefinitions.get() != null;
+
+ final var map = overrides.get();
+
+ if( map != null ) {
+ mDefinitions.get().putAll( map );
+ }
+ }
+
+ /**
+ * Sets the source for deriving the {@link Caret}. Typically, this is
+ * the text editor that has focus.
+ *
+ * @param caret The source for the currently active caret.
+ */
+ public void setCaret( final Supplier<Caret> caret ) {
+ assert caret != null;
+ mCaret = caret;
+ }
+
+ public void setSigilBegan( final Supplier<String> sigilBegan ) {
+ assert sigilBegan != null;
+ mSigilBegan = sigilBegan;
+ }
+
+ public void setSigilEnded( final Supplier<String> sigilEnded ) {
+ assert sigilEnded != null;
+ mSigilEnded = sigilEnded;
+ }
+
+ public void setRWorkingDir( final Supplier<Path> rWorkingDir ) {
+ assert rWorkingDir != null;
+ mRWorkingDir = rWorkingDir;
+ }
+
+ public void setRScript( final Supplier<String> rScript ) {
+ assert rScript != null;
+ mRScript = rScript;
+ }
+
+ public void setCurlQuotes( final Supplier<Boolean> curlQuotes ) {
+ assert curlQuotes != null;
+ mCurlQuotes = curlQuotes;
+ }
+
+ public void setAutoRemove( final Supplier<Boolean> autoRemove ) {
+ assert autoRemove != null;
+ mAutoRemove = autoRemove;
+ }
+
+ private boolean isExportFormat( final ExportFormat format ) {
+ return mExportFormat == format;
+ }
+ }
+
+ public static GenericBuilder<Mutator, ProcessorContext> builder() {
+ return GenericBuilder.of( Mutator::new, ProcessorContext::new );
+ }
+
+ /**
+ * Creates a new context for use by the {@link ProcessorFactory} when
+ * instantiating new {@link Processor} instances. Although all the
+ * parameters are required, not all {@link Processor} instances will use
+ * all parameters.
+ */
+ private ProcessorContext( final Mutator mutator ) {
+ assert mutator != null;
+
+ mMutator = mutator;
+ }
+
+ public Path getSourcePath() {
+ return mMutator.mSourcePath;
+ }
+
+ /**
+ * Answers what type of input document is to be processed.
+ *
+ * @return The input document's {@link MediaType}.
+ */
+ public MediaType getSourceType() {
+ return MediaTypeExtension.fromPath( mMutator.mSourcePath );
+ }
+
+ /**
+ * Fully qualified file name to use when exporting (e.g., document.pdf).
+ *
+ * @return Full path to a file name.
+ */
+ public Path getTargetPath() {
+ return mMutator.mTargetPath;
+ }
+
+ public ExportFormat getExportFormat() {
+ return mMutator.mExportFormat;
+ }
+
+ public Locale getLocale() {
+ return mMutator.mLocale.get();
+ }
+
+ /**
+ * Returns the variable map of definitions, without interpolation.
+ *
+ * @return A map to help dereference variables.
+ */
+ public Map<String, String> getDefinitions() {
+ return mMutator.mDefinitions.get();
+ }
+
+ /**
+ * Returns the variable map of definitions, with interpolation.
+ *
+ * @return A map to help dereference variables.
+ */
+ public InterpolatingMap getInterpolatedDefinitions() {
+ return new InterpolatingMap(
+ createDefinitionKeyOperator(), getDefinitions()
+ ).interpolate();
+ }
+
+ public Map<String, String> getMetadata() {
+ return mMutator.mMetadata.get();
+ }
+
+ /**
+ * Returns the current caret position in the document being edited and is
+ * always up-to-date.
+ *
+ * @return Caret position in the document.
+ */
+ public Supplier<Caret> getCaret() {
+ return mMutator.mCaret;
+ }
+
+ /**
+ * Returns the directory that contains the file being edited. When
+ * {@link Constants#DOCUMENT_DEFAULT} is created, the parent path is
+ * {@code null}. This will get absolute path to the file before trying to
+ * get te parent path, which should always be a valid path. In the unlikely
+ * event that the base path cannot be determined by the path alone, the
+ * default user directory is returned. This is necessary for the creation
+ * of new files.
+ *
+ * @return Path to the directory containing a file being edited, or the
+ * default user directory if the base path cannot be determined.
+ */
+ public Path getBaseDir() {
+ final var path = getSourcePath().toAbsolutePath().getParent();
+ return path == null ? DEFAULT_DIRECTORY : path;
+ }
+
+ FileType getSourceFileType() {
+ return lookup( getSourcePath() );
+ }
+
+ public Path getThemeDir() {
+ return mMutator.mThemeDir.get();
+ }
+
+ public Path getImageDir() {
+ return mMutator.mImageDir.get();
+ }
+
+ public Path getCacheDir() {
+ return mMutator.mCacheDir.get();
+ }
+
+ public Iterable<String> getImageOrder() {
+ assert mMutator.mImageOrder != null;
+
+ final var order = mMutator.mImageOrder.get();
+ final var token = order.contains( "," ) ? ',' : ' ';
+
+ return Splitter.on( token ).split( token + order );
+ }
+
+ public String getImageServer() {
+ return mMutator.mImageServer.get();
+ }
+
+ public Path getFontDir() {
+ return mMutator.mFontDir.get();
+ }
+
+ public String getModesEnabled() {
+ final var processor = new VariableProcessor( IDENTITY, this );
+ final var needles = processor.getDefinitions();
+ final var haystack = mMutator.mModesEnabled.get();
+ return needles.containsKey( haystack )
+ ? replace( haystack, needles )
+ : haystack;
}
src/main/java/com/keenwrite/typesetting/Typesetter.java
private Path mCacheDir = USER_CACHE_DIR.toPath();
private Path mFontDir = getFontDirectory().toPath();
- private String mEnableMode = "";
+ private String mModesEnabled = "";
private boolean mAutoRemove;
}
- public void setEnableMode( final String enableMode ) {
- mEnableMode = enableMode;
+ public void setModesEnabled( final String modesEnabled ) {
+ mModesEnabled = modesEnabled;
}
}
- public String getEnableMode() {
- return mEnableMode;
+ public String getModesEnabled() {
+ return mModesEnabled;
}
args.add( sourcePath );
- final var enableMode = getEnableMode();
+ final var modesEnabled = getModesEnabled();
- if( !enableMode.isBlank() ) {
- args.add( format( "--mode=%s", enableMode ) );
+ if( !modesEnabled.isBlank() ) {
+ args.add( format( "--mode=%s", modesEnabled ) );
}
}
- protected String getEnableMode() {
- return mMutator.getEnableMode();
+ protected String getModesEnabled() {
+ return mMutator.getModesEnabled();
}

Fixes command-line usage of modes enabled

Author DaveJarvis <email>
Date 2024-01-18 17:12:16 GMT-0800
Commit e8869a0f2286a3805310fad35b340b41b357a2a3
Parent 7d8ffa7
Delta 347 lines added, 354 lines removed, 7-line decrease