package Breccia.Web.imager;
import Java.GraphemeClusterCounter;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import Java.Unhandled;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import static Breccia.Web.imager.Project.zeroBased;
import static Breccia.Web.imager.ReferenceTranslation.newTranslation;
import static Java.IntralineCharacterPointer.markedLine;
import static java.lang.Float.parseFloat;
import static java.lang.System.err;
import static java.lang.System.getProperty;
import static java.nio.file.Files.readString;
import static Java.Paths.toPath;
import static Java.URI_References.enslash;
import static Java.URI_References.isRemote;
import static java.util.Collections.unmodifiableList;
/** @see
* Options for the `breccia-web-image` command
*/
public class ImagingOptions extends Options {
public ImagingOptions( String commandName ) { super( commandName ); } // [SLA]
/** The home directory of the source author.
*
* @see
* Command option `-author-home-directory`
*/
public final Path authorHomeDirectory() { return authorHomeDirectory; }
/** The columnar offset on which to centre the text.
*
* @see
* Command option `-centre-column`
*/
public final float centreColumn() { return centreColumn; }
/** The enslashed name of the directory containing the auxiliary files of the Web image.
*
* @see
* Command option `-co-service-directory`
* @see Java.Path#enslash(String)
*/
public final String coServiceDirectory() { return coServiceDirectory; }
/** List of occurences of the `-exclude` option, each in the form of a pattern matcher.
*
* @see
* Command option `-exclude`
*/
public final List exclusions() { return exclusions; }
/** The font file for glyph tests.
*
* @see
* Command option `-glyph-test-font`
*/
public final String glyphTestFont() { return glyphTestFont; }
/** List of occurences of the `-reference-mapping` option, each itself a list
* of reference translations.
*
* @see
* Command option `-reference-mapping`
*/
public final List> referenceMappings() { return referenceMappings; }
/** Whether to run without effect.
*
* @see
* Command option `-fake`
*/
public final boolean toFake() { return toFake; }
/** Whether to forcefully remake the Web image.
*
* @see
* Command option `-force`
*/
public final boolean toForce() { return toForce; }
/** Whether to image mathematic expressions using MathJax.
*
* @see
* Command option `-math`
*/
public final boolean toImageMath() { return toImageMath; }
// ━━━ O p t i o n s ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
public @Override void initialize( final List args ) {
super.initialize( args );
if( authorHomeDirectory == null ) authorHomeDirectory = Path.of( getProperty( "user.home" ));
if( glyphTestFont == null ) {
if( !isRemote( coServiceDirectory )) {
final Path styleSheet; {
final URI uCSD; { /* Expose any syntax error in the value of `--co-service-directory`
before complicating the error message by involving the style sheet. */
try { uCSD = new URI( coServiceDirectory ); }
catch( URISyntaxException x ) { throw new Unhandled( x ); }
toPath( uCSD ); } // May throw `IllegalArgumentException`.
styleSheet = toPath( uCSD.resolve( "Breccia/Web/imager/image.css" )); }
glyphTestFont = glyphTestFont( styleSheet );
if( glyphTestFont == null ) glyphTestFont = "none"; }
else glyphTestFont = "none";
out(2).println( "Glyph-test font: " + glyphTestFont ); }
assert referenceMappings instanceof ArrayList; // Yet to be initialized, that is.
referenceMappings = unmodifiableList( referenceMappings ); }
//// P r i v a t e ////////////////////////////////////////////////////////////////////////////////////
private Path authorHomeDirectory;
private float centreColumn = 52.5f;
private String coServiceDirectory = "http://reluk.ca/_/Web_service/";
private List exclusions = new ArrayList<>( /*initial capacity*/8 );
private String font = "FairfaxHD.ttf";
private String glyphTestFont;
/** @return The path of the designated glyph-test font, or null if none was found.
*/
private static String glyphTestFont( final Path styleSheet ) {
final Path directory = styleSheet.getParent();
final String content; {
try { content = readString( styleSheet ); }
catch( IOException x ) { throw new Unhandled( x ); }}
Matcher m;
// Imported sheets, recursively searching these first
// ───────────────
m = importedStyleSheetPattern.matcher( content );
while( m.find() ) {
final String importedSheet = m.group( 2 );
if( !isRemote( importedSheet )) {
final String f = glyphTestFont( directory.resolve( importedSheet ));
if( f != null ) return f; }}
// Present sheet
// ─────────────
m = glyphTestFontSrcPattern.matcher( content );
if( m.find() ) {
final String src = m.group( 2 );
if( !isRemote( src )) return directory.resolve(src).toString(); }
return null; }
/** A pattern to `find` in a style sheet the designated glyph-test font. It captures as group (2)
* the font reference, formally a URI reference.
*
* @see java.util.regex.Matcher#find()
* @see Font reference
* @see
* URI generic syntax §4.1, URI reference
*/
private static final Pattern glyphTestFontSrcPattern = Pattern.compile(
"(['\"])(\\S+?)\\1 */\\* *\\[GTF\\]" ); // As per note GTF in `image.css`.
/** A pattern to `find` in a style sheet the import of another style sheet. It captures as group (2)
* the ‘URL of the style sheet to be imported’, formally a URI reference.
*
* @see java.util.regex.Matcher#find()
* @see Importing style sheets
* @see
* URI generic syntax §4.1, URI reference
*/
private static final Pattern importedStyleSheetPattern = Pattern.compile(
"@import +(?:(?:url|src)\\()?(['\"])(.+?)\\1" );
/** @param arg A nominal argument, aka option.
* @param prefix The leading name and equals sign, e.g. "foo=".
* @param p Offset after `prefix` of the pattern that `x` complains of.
* If the pattern directly follows the prefix, then `p` is zero.
*/
String message( final String arg, final String prefix, final int p,
final PatternSyntaxException x ) {
final int a = prefix.length() + p + zeroBased( x.getIndex() ); // Offset in `arg` of the fault.
return commandName + ": Malformed pattern: " + x.getDescription() + '\n'
+ markedLine( " ", arg, a, new GraphemeClusterCounter() ); }
private List> referenceMappings = new ArrayList<>(
/*initial capacity*/4 );
/** A pattern for `lookingAt` a reference translation within a `-reference-mapping` option.
* It captures as group (2) the translation pattern, and group (3) the replacement string.
*
* @see java.util.regex.Matcher#find()
* @see
* Command option `-reference-mapping`
*/
private static final Pattern referenceTranslationPattern = Pattern.compile( "(.)(.+?)\\1(.+?)\\1" );
private boolean toFake;
private boolean toForce;
private boolean toImageMath;
// ━━━ O p t i o n s ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
protected @Override boolean initialize( final String arg ) {
boolean isGo = true;
String s;
arg:if( arg.startsWith( s = "-author-home-directory=" )) {
authorHomeDirectory = Path.of( value( arg, s )); }
else if( arg.startsWith( s = "-centre-column=" )) centreColumn = parseFloat( value( arg, s ));
else if( arg.startsWith( s = "-co-service-directory=" )) {
coServiceDirectory = enslash( value( arg, s )); }
else if( arg.startsWith( s = "-exclude=" )) {
final Pattern pattern; {
try { pattern = Pattern.compile( value( arg, s )); }
catch( final PatternSyntaxException x ) {
err.println( message( arg, s, 0, x ));
isGo = false;
break arg; }}
exclusions.add( pattern.matcher("") ); }
else if( arg.equals( "-fake" )) toFake = true;
else if( arg.equals( "-force" )) toForce = true;
else if( arg.startsWith( s = "-glyph-test-font=" )) glyphTestFont = value( arg, s );
else if( arg.equals( "-math" )) toImageMath = true;
else if( arg.startsWith( s = "-reference-mapping=" )) {
final List tt = new ArrayList<>( /*initial capacity*/8 );
tt: {
final String value = value( arg, s );
final Matcher m = referenceTranslationPattern.matcher( value );
final int vN = value.length();
int v = 0;
while( m.lookingAt() ) {
final Pattern pattern; {
try { pattern = Pattern.compile( m.group( 2 )); }
catch( final PatternSyntaxException x ) {
err.println( message( arg, s, m.start(2), x ));
isGo = false;
break arg; }}
tt.add( newTranslation( pattern, /*replacement*/m.group(3) ));
v = m.end();
if( value.startsWith( "||"/*separator*/, v )) {
v += 2;
if( v == vN ) break tt; } // Trailing separator, a trivial violation. Allow it.
else break;
m.region( v, vN ); }
if( v < vN ) {
err.println( commandName + ": Malformed translation: \n"
+ markedLine( " ", arg, s.length() + v, new GraphemeClusterCounter() ));
isGo = false;
break arg; }
else assert v == vN; }
referenceMappings.add( unmodifiableList( tt )); }
else isGo = super.initialize( arg );
return isGo; }}
// NOTE
// ────
// SLA Source-launch access. This member would have `protected` access were it not needed by
// class `BrecciaWebImageCommand`. Source launched and loaded by a separate class loader,
// that class is treated at runtime as residing in a separate package.
// Copyright © 2022-2024 Michael Allan. Licence MIT.