From fc27d04c4788948cce4b9d43ac78d895376c24e2 Mon Sep 17 00:00:00 2001 From: Bert Frees Date: Mon, 11 Nov 2024 19:48:42 +0100 Subject: [PATCH 01/15] git subrepo pull framework subrepo: subdir: "framework" merged: "f64694e84e" upstream: origin: "git@github.com:daisy/pipeline-framework.git" branch: "master" commit: "f64694e84e" git-subrepo: version: "0.3.1" origin: "???" commit: "???" --- framework/.gitrepo | 4 +- framework/bom/pom.xml | 4 +- .../main/java/org/daisy/common/file/URLs.java | 2 +- framework/modules-registry/pom.xml | 14 +- .../org/daisy/pipeline/modules/Component.java | 113 +++-- .../daisy/pipeline/modules/Dependency.java | 4 + .../org/daisy/pipeline/modules/Entity.java | 77 +-- .../pipeline/modules/JavaDependency.java | 23 + .../org/daisy/pipeline/modules/Module.java | 338 +++++++++++--- .../pipeline/modules/ModuleRegistry.java | 40 +- .../pipeline/modules/RelaxNGResource.java | 118 +++++ .../pipeline/modules/ResolutionException.java | 23 + .../pipeline/modules/ResourceLoader.java | 14 +- .../pipeline/modules/UseXSLTPackage.java | 27 ++ .../daisy/pipeline/modules/XProcResource.java | 174 +++++++ .../daisy/pipeline/modules/XSLTPackage.java | 78 ++++ .../daisy/pipeline/modules/XSLTResource.java | 193 ++++++++ .../modules/impl/DefaultModuleRegistry.java | 441 ++++++++++++++++++ .../{resolver => }/ModuleUriResolver.java | 57 ++- .../modules/impl/resolver/package-info.java | 4 - .../impl/tracker/DefaultModuleRegistry.java | 104 ----- .../modules/impl/tracker/package-info.java | 4 - .../daisy/pipeline/modules/ModuleTest.java | 17 +- framework/parent/pom.xml | 2 +- framework/saxon-adapter/pom.xml | 14 +- .../daisy/common/saxon/SaxonConfigurator.java | 33 ++ 26 files changed, 1531 insertions(+), 391 deletions(-) create mode 100644 framework/modules-registry/src/main/java/org/daisy/pipeline/modules/Dependency.java create mode 100644 framework/modules-registry/src/main/java/org/daisy/pipeline/modules/JavaDependency.java create mode 100644 framework/modules-registry/src/main/java/org/daisy/pipeline/modules/RelaxNGResource.java create mode 100644 framework/modules-registry/src/main/java/org/daisy/pipeline/modules/ResolutionException.java create mode 100644 framework/modules-registry/src/main/java/org/daisy/pipeline/modules/UseXSLTPackage.java create mode 100644 framework/modules-registry/src/main/java/org/daisy/pipeline/modules/XProcResource.java create mode 100644 framework/modules-registry/src/main/java/org/daisy/pipeline/modules/XSLTPackage.java create mode 100644 framework/modules-registry/src/main/java/org/daisy/pipeline/modules/XSLTResource.java create mode 100644 framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/DefaultModuleRegistry.java rename framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/{resolver => }/ModuleUriResolver.java (71%) delete mode 100644 framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/resolver/package-info.java delete mode 100644 framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/tracker/DefaultModuleRegistry.java delete mode 100644 framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/tracker/package-info.java diff --git a/framework/.gitrepo b/framework/.gitrepo index a2153503a5..b8147b5836 100644 --- a/framework/.gitrepo +++ b/framework/.gitrepo @@ -6,6 +6,6 @@ [subrepo] remote = git@github.com:daisy/pipeline-framework.git branch = master - commit = 1614556e78510068f584bba4ba44ef383007a117 - parent = 3e05d8023a313a9082b95541ac98886b0e04d6da + commit = f64694e84ec3196f980bbc1737e0b11ef169d3ce + parent = 4fa2144e11e847611b27c8c10afce83e2e447574 cmdver = 0.3.1 diff --git a/framework/bom/pom.xml b/framework/bom/pom.xml index 74222e6977..9e54f82a2d 100644 --- a/framework/bom/pom.xml +++ b/framework/bom/pom.xml @@ -70,7 +70,7 @@ org.daisy.pipeline modules-registry - 4.1.2 + 5.0.0-SNAPSHOT org.daisy.pipeline @@ -80,7 +80,7 @@ org.daisy.pipeline saxon-adapter - 5.0.0 + 5.5.0-SNAPSHOT org.daisy.pipeline diff --git a/framework/common-utils/src/main/java/org/daisy/common/file/URLs.java b/framework/common-utils/src/main/java/org/daisy/common/file/URLs.java index d72871d146..4c96077ea4 100644 --- a/framework/common-utils/src/main/java/org/daisy/common/file/URLs.java +++ b/framework/common-utils/src/main/java/org/daisy/common/file/URLs.java @@ -143,7 +143,7 @@ public static URI relativize(URI base, URI url) { return base.relativize(url); } - private static Map fsEnv = Collections.emptyMap(); + private static final Map fsEnv = Collections.emptyMap(); /** * @param resource The (not URL-encoded) path of a resource inside the specified JAR or class directory diff --git a/framework/modules-registry/pom.xml b/framework/modules-registry/pom.xml index 1fa76fce25..bf20eb81a7 100644 --- a/framework/modules-registry/pom.xml +++ b/framework/modules-registry/pom.xml @@ -11,7 +11,7 @@ modules-registry - 4.1.3-SNAPSHOT + 5.0.0-SNAPSHOT bundle DAISY Pipeline 2 :: Module Registry @@ -30,10 +30,10 @@ slf4j-api - + org.osgi org.osgi.core - provided org.osgi @@ -71,8 +71,8 @@ <_dsannotations> - org.daisy.pipeline.modules.impl.resolver.ModuleUriResolver, - org.daisy.pipeline.modules.impl.tracker.DefaultModuleRegistry, + org.daisy.pipeline.modules.impl.ModuleUriResolver, + org.daisy.pipeline.modules.impl.DefaultModuleRegistry, org.daisy.pipeline.xmlcatalog.impl.StaxXmlCatalogParser !org.daisy.common.spi,* @@ -97,8 +97,8 @@ - org.daisy.pipeline.modules.impl.resolver.ModuleUriResolver, - org.daisy.pipeline.modules.impl.tracker.DefaultModuleRegistry, + org.daisy.pipeline.modules.impl.ModuleUriResolver, + org.daisy.pipeline.modules.impl.DefaultModuleRegistry, org.daisy.pipeline.xmlcatalog.impl.StaxXmlCatalogParser diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/Component.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/Component.java index 68715a6d44..5a2001a019 100644 --- a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/Component.java +++ b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/Component.java @@ -2,48 +2,35 @@ import java.net.URI; import java.net.URL; +import java.nio.file.NoSuchFileException; import org.daisy.common.file.URLs; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * Module component now based on expath package components. */ -public class Component { - - +public class Component implements Dependency { - /** The uri. */ + private final Module module; private final URI uri; - - /** The path. */ - private final String path; - // private Space space; - /** The loader. */ - private final ResourceLoader loader; - - /** The module. */ - private Module module; - - /** The m logger. */ - Logger mLogger = LoggerFactory.getLogger(getClass().getName()); + private final URL resource; + private final String version; /** * Instantiates a new component. - * - * @param uri - * the uri - * @param path - * the path - * @param loader - * the loader which actually able to load the resource. */ - public Component(URI uri, String path, ResourceLoader loader) { + public Component(Module module, URI uri, String path) throws NoSuchFileException { + this.module = module; this.uri = uri; - this.path = path; - this.loader = loader; + resource = module.getResource(path); + version = module.getVersion().replaceAll("-SNAPSHOT$", ""); + } + + public Component(Module module, URI uri, URL resource) { + this.module = module; + this.uri = uri; + this.resource = resource; + version = module.getVersion().replaceAll("-SNAPSHOT$", ""); } /** @@ -55,30 +42,30 @@ public URI getURI() { return uri; } - /* - * public Space getSpace() { return space; } - */ - /** * Gets the resource's real uri. * * @return the resource */ public URI getResource() { - try { + return URLs.asURI(resource); + } - mLogger.trace("Getting resource from component " + this + ": " + path); - URL url= loader.loadResource(path); - if(url!=null) { - return URLs.asURI(url); - } else { - return null; - } + /** + * Get the version of this component. + * + * Defaults to the version number of the module. Method should be overridden by components that + * have their own versioning. + */ + public String getVersion() { + return version; + } - } catch (Exception e) { - mLogger.debug("Resource " + path + " does not exist", e); - return null; - } + /** + * Gets the module owner of this component. + */ + public Module getModule() { + return module; } /* @@ -91,22 +78,28 @@ public String toString() { return "[" + uri + "]"; } - /** - * Gets the module owner of this component. - * - * @return the module - */ - public Module getModule() { - return module; + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null) + return false; + if (!(o instanceof Component)) + return false; + Component that = (Component)o; + if (!module.equals(that.module)) + return false; + if (!uri.equals(that.uri)) + return false; + return true; } - /** - * Sets the module. - * - * @param module - * the new module - */ - public void setModule(Module module) { - this.module = module; + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + module.hashCode(); + result = prime * result + uri.hashCode(); + return result; } } diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/Dependency.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/Dependency.java new file mode 100644 index 0000000000..db8aad3237 --- /dev/null +++ b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/Dependency.java @@ -0,0 +1,4 @@ +package org.daisy.pipeline.modules; + +public interface Dependency { +} \ No newline at end of file diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/Entity.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/Entity.java index bb57e81b17..7992111d18 100644 --- a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/Entity.java +++ b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/Entity.java @@ -2,63 +2,34 @@ import java.net.URI; import java.net.URL; +import java.nio.file.NoSuchFileException; import org.daisy.common.file.URLs; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - - -// TODO: Auto-generated Javadoc /** * Entity represents a public id and its path inside the module. Only handles * publicId as the privates one will be loaded as components. */ public class Entity { - /** The Public id. */ - private final String mPublicId; - - /** The Path. */ - private final String mPath; + private final Module module; + private final String publicId; + private final URL resource; - /** The loader. */ - private final ResourceLoader mLoader; - - /** The module. */ - private Module mModule; - - private static Logger mLogger = LoggerFactory.getLogger(Entity.class); /** * Instantiates a new entity. - * - * @param publicId the public id - * @param path the path - * @param loader the loader */ - public Entity(String publicId, String path, ResourceLoader loader) { - super(); - mPublicId = publicId; - mPath = path; - mLoader = loader; + public Entity(Module module, String publicId, String path) throws NoSuchFileException { + this.module = module; + this.publicId = publicId; + resource = module.getResource(path); } /** - * Gets the module. - * - * @return the module + * Gets the module owner of this entity. */ public Module getModule() { - return mModule; - } - - /** - * Sets the module. - * - * @param module the new module - */ - public void setModule(Module module) { - mModule = module; + return module; } /** @@ -67,7 +38,7 @@ public void setModule(Module module) { * @return the public id */ public String getPublicId() { - return mPublicId; + return publicId; } /** @@ -76,30 +47,6 @@ public String getPublicId() { * @return the path */ public URI getResource() { -try { - - mLogger.trace("Getting resource from entity " + this + ": " + mPath); - URL url= mLoader.loadResource(mPath); - if(url!=null) { - return URLs.asURI(url); - } else { - return null; - } - - } catch (Exception e) { - mLogger.debug("Resource " + mPath + " does not exist", e); - return null; - } - } - - /** - * Gets the loader. - * - * @return the loader - */ - public ResourceLoader getLoader() { - return mLoader; + return URLs.asURI(resource); } - - } diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/JavaDependency.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/JavaDependency.java new file mode 100644 index 0000000000..bcc32682a4 --- /dev/null +++ b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/JavaDependency.java @@ -0,0 +1,23 @@ +package org.daisy.pipeline.modules; + +/** + * Java dependency of an XSLT resource. The Java resource (class) is assumed to live in the same + * module as the XSLT resource. + */ +public class JavaDependency implements Dependency { + + private final Module module; + private final String className; + + /** + * @param className The fully qualified name of the class. + */ + public JavaDependency(Module module, String className) { + this.module = module; + this.className = className; + } + + public String getClassName() { + return className; + } +} diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/Module.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/Module.java index fc78b77875..f920cf3453 100644 --- a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/Module.java +++ b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/Module.java @@ -1,15 +1,27 @@ package org.daisy.pipeline.modules; import java.io.File; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; +import java.util.function.Supplier; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import javax.xml.stream.XMLInputFactory; import com.google.common.collect.Iterators; @@ -32,71 +44,109 @@ public abstract class Module { private String name; private String version; private String title; - private final Map components = new HashMap<>(); - private final Map entities = new HashMap<>(); + protected final Map> components = new HashMap<>(); + private final Map> entities = new HashMap<>(); + private final Map xsltPackages = new HashMap<>(); + private ResourceLoader loader; private static final Logger mLogger = LoggerFactory.getLogger(Module.class); + private static final Map fsEnv = Collections.emptyMap(); + /** * Instantiate a new module */ protected Module(String name, String version, String title) { - this.name = name; - this.version = version; - this.title = title; - } - - /** - * Initialize the module - */ - protected void init(XmlCatalogParser parser) { - Class thisClass = this.getClass(); - try { - URI jarFileURI = thisClass.getProtectionDomain().getCodeSource().getLocation().toURI(); + if (OSGiHelper.inOSGiContext()) + OSGiHelper.populate(this); + else { + this.name = name; + this.version = version; + this.title = title; try { + URI jarFileURI = getClass().getProtectionDomain().getCodeSource().getLocation().toURI(); + if (!jarFileURI.toString().startsWith("file:")) + throw new RuntimeException("unexpected code source location: " + jarFileURI); File jarFile = new File(jarFileURI); - mLogger.trace("Creating module from JAR: " + jarFile); - parseCatalog( - parser, - new ResourceLoader() { + if (!jarFile.exists()) + throw new RuntimeException("coding error"); + getLogger().trace("Creating module from JAR: " + jarFile); + this.loader = new ResourceLoader() { // Can't use ClassLoader.getResource() because there can be name // clashes between resources in different JARs. Alternative // solution would be to have a ClassLoader for each JAR. @Override - public URL loadResource(String path) { + public URL loadResource(String path) throws NoSuchFileException { // Paths are assumed to be relative to META-INF if (!path.startsWith("../")) { throw new RuntimeException("Paths must start with '../' but got '" + path + "'"); } - path = path.substring(2); - try { - return jarFile.isDirectory() ? - new URL(jarFile.toURI().toASCIIString() + path) : - new URL("jar:" + jarFile.toURI().toASCIIString() + "!" + path); - } catch (MalformedURLException e) { - throw new RuntimeException(e); + path = path.substring(3); + if (jarFile.isDirectory()) { + File f = new File(jarFile, path); + if (!f.exists()) + throw new NoSuchFileException("file does not exist: " + f); + return URLs.asURL(f); + } else { + FileSystem fs; { + try { + fs = FileSystems.newFileSystem(URLs.asURI("jar:" + jarFileURI), fsEnv); } + catch (IOException e) { + throw new RuntimeException(e); }} + try { + Path f = fs.getPath("/" + path); + if (!Files.exists(f)) + throw new NoSuchFileException("file does not exist: " + f); + try { + return new URL("jar:" + jarFileURI + "!/" + path); } + catch (MalformedURLException e) { + throw new RuntimeException(e); }} + finally { + try { + fs.close(); } + catch (IOException e) { + throw new RuntimeException(e); }} } } @Override public Iterable loadResources(final String path) { throw new UnsupportedOperationException("Not supported without OSGi."); } - }); - } catch (IllegalArgumentException e) { - // Could be because we are running in OSGi context - OSGiHelper.init(this, parser); + }; + } catch (URISyntaxException e) { + throw new RuntimeException(e); } - } catch (URISyntaxException e) { - throw new RuntimeException(e); } } + protected Module(String name, String version, String title, ResourceLoader loader) { + if (OSGiHelper.inOSGiContext()) + throw new RuntimeException("Constructor not supported in OSGi"); + this.name = name; + this.version = version; + this.title = title; + this.loader = loader; + } + + protected Logger getLogger() { + return mLogger; + } + // static nested class in order to delay class loading private static abstract class OSGiHelper { - static void init(Module thiz, XmlCatalogParser parser) { + + static boolean inOSGiContext() { + try { + return FrameworkUtil.getBundle(OSGiHelper.class) != null; + } catch (NoClassDefFoundError e) { + return false; + } + } + + static void populate(Module thiz) { Bundle bundle = FrameworkUtil.getBundle(thiz.getClass()); - mLogger.trace("Creating module from OSGi bundle: " + bundle); - ResourceLoader loader = new ResourceLoader() { + thiz.getLogger().trace("Creating module from OSGi bundle: " + bundle); + thiz.loader = new ResourceLoader() { @Override public URL loadResource(String path) { // Paths are assumed to be relative to META-INF @@ -118,7 +168,6 @@ public Iterator iterator() { }; } }; - thiz.parseCatalog(parser, loader); // these fields are already set, but now get the values from the bundle metadata thiz.name = bundle.getHeaders().get("Bundle-Name").toString(); thiz.version = bundle.getVersion().toString(); @@ -126,55 +175,103 @@ public Iterator iterator() { } } + /** + * Called by {@link ModuleRegistry} to resolve dependencies of components. If a dependency of a + * component can not be resolved, this does not result in an exception, but the component will + * not be created (and consequently will not be available through the {@link #getComponent()} or + * {@link #getEntity} method). + */ + public abstract void resolveDependencies(); + /** * Parse catalog.xml file */ - private void parseCatalog(XmlCatalogParser parser, ResourceLoader loader) { - URL catalogURL = loader.loadResource("../META-INF/catalog.xml"); - if (catalogURL == null) - throw new RuntimeException("/META-INF/catalog.xml file not found"); + public static void parseCatalog(Module module, XmlCatalogParser parser) { + URL catalogURL; { + try { + catalogURL = module.loader.loadResource("../META-INF/catalog.xml"); + } catch (NoSuchFileException e) { + throw new RuntimeException("/META-INF/catalog.xml file not found", e); + } + } XmlCatalog catalog = parser.parse(URLs.asURI(catalogURL)); - parseCatalog(catalog, loader); + try { + parseCatalog(module, catalog); + } catch (Throwable e) { + throw new RuntimeException("catalog.xml can not be parsed", e); + } } - // package private for unit tests - void parseCatalog(XmlCatalog catalog, ResourceLoader loader) { + public static void parseCatalog(Module module, XmlCatalog catalog) throws NoSuchFileException { for (Map.Entry entry : catalog.getSystemIdMappings().entrySet()) { - addComponent(entry.getKey(), entry.getValue().toString(), loader); + URI path = entry.getValue(); + if (path.isAbsolute() || path.getSchemeSpecificPart().startsWith("/")) + throw new NoSuchFileException("Expected a relative path, but got: " + path); + module.addComponent(entry.getKey(), path.getPath()); } for (Map.Entry entry : catalog.getUriMappings().entrySet()) { - addComponent(entry.getKey(), entry.getValue().toString(), loader); + URI path = entry.getValue(); + if (path.isAbsolute() || path.getSchemeSpecificPart().startsWith("/")) + throw new NoSuchFileException("Expected a relative path, but got: " + path); + module.addComponent(entry.getKey(), path.getPath()); } for (Map.Entry entry : catalog.getPublicMappings().entrySet()) { - addEntity(entry.getKey(), entry.getValue().toString(), loader); + URI path = entry.getValue(); + if (path.isAbsolute() || path.getSchemeSpecificPart().startsWith("/")) + throw new NoSuchFileException("Expected a relative path, but got: " + path); + module.addEntity(entry.getKey(), path.getPath()); } for (Map.Entry rule : catalog.getRewriteUris().entrySet()) { - Iterable entries = loader.loadResources(rule.getValue().toString()); + URI path = rule.getValue(); + if (path.isAbsolute() || path.getSchemeSpecificPart().startsWith("/")) + throw new NoSuchFileException("Expected a relative path, but got: " + path); + Iterable entries = module.loader.loadResources(path.getPath()); for (URL url : entries) { try { // get tail of the path i.e. ../static/css/ -> /css/ - String path = url.toURI().getPath().toString().replace(rule.getValue().toString().replace("..",""),""); - addComponent(rule.getKey().resolve(URI.create(path)), url.toString(), loader); + String rewritten = url.toURI().getPath().replace(path.getPath().replace("..", ""), ""); + module.addComponent(new Component(module, rule.getKey().resolve(URI.create(rewritten)), url)); } catch (URISyntaxException e) { - mLogger.warn("Exception while generating paths"); + module.getLogger().warn("Exception while generating paths"); } } } } - private void addComponent(URI uri, String path, ResourceLoader loader) { - mLogger.trace("add component:" + uri.toString() + ", path: " + path); - Component component = new Component(uri, path, loader); - component.setModule(this); - components.put(component.getURI(), component); + /** + * This method can be overridden to exclude certain components that have unmet dependencies. + */ + protected boolean addComponent(URI uri, String path) throws NoSuchFileException { + getLogger().trace("Adding component: " + uri.toString() + ", path: " + path); + return addComponent(new Component(this, uri, path)); + } + + protected boolean addComponent(Component component) { + components.put(component.getURI(), () -> component); + return true; } - - - private void addEntity(String publicId, String path, ResourceLoader loader) { - mLogger.trace("add entity:" + publicId.toString() + ", path: " + path); - Entity entity = new Entity(publicId, path, loader); - entity.setModule(this); - entities.put(entity.getPublicId(), entity); + + protected boolean addEntity(String publicId, String path) throws NoSuchFileException { + getLogger().trace("Adding entity: " + publicId.toString() + ", path: " + path); + return addEntity(new Entity(this, publicId, path)); + } + + protected boolean addEntity(Entity entity) { + entities.put(entity.getPublicId(), () -> entity); + return true; + } + + protected boolean addXSLTPackage(String path, XMLInputFactory parser) throws NoSuchFileException, IllegalArgumentException { + return addXSLTPackage(new XSLTPackage(this, path, parser)); + } + + protected boolean addXSLTPackage(String name, String version, String path) throws NoSuchFileException { + return addXSLTPackage(new XSLTPackage(this, name, version, path)); + } + + protected boolean addXSLTPackage(XSLTPackage pack) { + xsltPackages.put(pack.getName(), pack); + return true; } /** @@ -199,35 +296,128 @@ public String getTitle() { } /** - * Gets the components. + * Get the specified resource from the module. This includes all resources, also the ones that + * are not exposed as components. The method can be overridden to exclude certain resources that + * have unmet dependencies. + * + * @param path The (not URL-encoded) path, relative to catalog.xml, of a resource inside the JAR + * or class directory of the module. + * @return An encoded absolute URL + * @throws NoSuchFileException if the resource at {@code path} is not available */ - public Iterable getComponents() { - return components.values(); + public URL getResource(String path) throws NoSuchFileException { + return loader.loadResource(path); } /** - * Gets the component identified by the given uri. + * Whether this module provides a component with the given URI. This assumes that the component + * can be resolved. + * + * If the component could not be resolved (e.g. due to a missing dependency), this will result + * in a {@link ResolutionException} when the {@link #getComponent} method is called. */ - public Component getComponent(URI uri) { - return components.get(uri); + public boolean hasComponent(URI uri) { + return components.containsKey(uri); } /** - * Gets the list of entities. + * Gets all the component URIs. */ - public Iterable getEntities() { - return entities.values(); + public Set getComponents() { + return components.keySet(); } /** - * Gets the entity identified by the given public id. + * Gets the component identified by the given URI. + * + * @throws NoSuchElementException if this module provides a component with the given URI. + * @throws ResolutionException if the component could not be resolved, e.g. due to a missing dependency. */ - public Entity getEntity(String publicId) { - return entities.get(publicId); + public Component getComponent(URI uri) throws NoSuchElementException, ResolutionException { + Supplier s = components.get(uri); + if (s == null) + throw new NoSuchElementException(); + Component c = s.get(); + if (c == null) + throw new ResolutionException(); + return c; + } + + /** + * Gets the list of entity IDs. + */ + public Set getEntities() { + return entities.keySet(); + } + + /** + * Whether this module provides a entity with the given ID. This assumes that the entity can be + * resolved. + * + * If the entity could not be resolved (e.g. due to a missing dependency), this will result in a + * {@link ResolutionException} when the {@link #getEntity} method is called. + */ + public boolean hasEntity(String publicId) { + return entities.containsKey(publicId); + } + + /** + * Gets the entity identified by the given ID. + * + * @throws NoSuchElementException if this module provides an entity with the given ID. + * @throws ResolutionException if the entity could not be resolved, e.g. due to a missing dependency. + */ + public Entity getEntity(String publicId) throws NoSuchElementException, ResolutionException { + Supplier s = entities.get(publicId); + if (s == null) + throw new NoSuchElementException(); + Entity e = s.get(); + if (e == null) + throw new ResolutionException(); + return e; + } + + /** + * Gets the list of XSLT packages. + */ + public Iterable getXSLTPackages() { + return xsltPackages.values(); + } + + /** + * Gets the XSLT package identified by the given name. + */ + public XSLTPackage getXSLTPackage(String name) { + return xsltPackages.get(name); } @Override public String toString() { return getName() + " [" + getVersion() + "]"; } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null) + return false; + if (!(o instanceof Module)) + return false; + Module that = (Module)o; + if (!name.equals(that.name)) + return false; + if (!version.equals(that.version)) + return false; + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + name.hashCode(); + result = prime * result + version.hashCode(); + return result; + } } diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/ModuleRegistry.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/ModuleRegistry.java index b4b3586da9..6ac3e12944 100644 --- a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/ModuleRegistry.java +++ b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/ModuleRegistry.java @@ -2,9 +2,6 @@ import java.net.URI; - - -// TODO: Auto-generated Javadoc /** * ModuleRegistry offers the functionality to enregister and query for the modules loaded. */ @@ -12,42 +9,43 @@ public interface ModuleRegistry extends Iterable { /** * Gets the module which has a component identified by the unique systemId. - * - * @param uri the uri - * @return the module by component */ public Module getModuleByComponent(URI uri); /** - * Gets the module which has declared the entity with the given public id. + * Gets the module which has a component identified by the unique systemId and a version + * matching the given version range. * - * @param publicId the public id - * @return the module by entity + * @param versionRange the version range, expressed as a mathematical interval notation, using the + * OSGi syntax. + */ + public Module getModuleByComponent(URI uri, String versionRange); + + /** + * Gets the module which has declared the entity with the given public id. */ public Module getModuleByEntity(String publicId); /** - * Gets the module handler which solves the dependency of the module source - * with the returned module component uri. - * - * @param component the component - * @param source the source - * @return the module + * Gets the module which has an XSLT package identified by the given name. */ - public Module resolveDependency(URI component, Module source); + public Module getModuleByXSLTPackage(String name); + + /** + * Gets the module which has the specified class among its resources, or {@code null} if no such + * module can be found. + */ + public Module getModuleByClass(Class clazz); /** * Returns the list of available components. - * - * @return an iterable with all the available components */ public Iterable getComponents(); /** * Returns the list of available entities. - * - * @return the entities */ public Iterable getEntities(); -} \ No newline at end of file +} diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/RelaxNGResource.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/RelaxNGResource.java new file mode 100644 index 0000000000..be37da5f97 --- /dev/null +++ b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/RelaxNGResource.java @@ -0,0 +1,118 @@ +package org.daisy.pipeline.modules; + +import java.io.InputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.nio.file.NoSuchFileException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.xml.namespace.QName; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.XMLEvent; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLInputFactory; + +import org.daisy.common.file.URLs; + +public class RelaxNGResource { + + private static final String RNG_NS = "http://relaxng.org/ns/structure/1.0"; + static final QName RNG_GRAMMAR = new QName(RNG_NS, "grammar"); + private static final QName RNG_INCLUDE = new QName(RNG_NS, "include"); + private static final QName _HREF = new QName("href"); + + private final URL resource; + + public RelaxNGResource(Module module, String path) throws NoSuchFileException { + this.resource = module.getResource(path); + } + + public RelaxNGResource(Component component) { + try { + this.resource = component.getResource().toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException("coding error"); + } + } + + /** + * List the dependencies of this resource. + * + * This recursively resolves all include that reference a component (either in the + * same module or in another one). Other dependencies are ignored. + * + * The dependencies are (external) {@link Component}. Included resources from the same module + * are handled recursively. + */ + public Set listDependencies(ModuleRegistry resolver, XMLInputFactory parser) { + return listDependencies(resource, resolver, parser); + } + + /** + * @param rngFile The RNG resource. + * @param resolver The module registry, for resolving dependency URIs. + * @param parser The StAX input factory for parsing the XML. + */ + static Set listDependencies(URL rngFile, ModuleRegistry resolver, XMLInputFactory parser) { + if (cache.containsKey(rngFile)) { + return cache.get(rngFile); + } else { + Set dependencies = new HashSet<>(); + try (InputStream is = rngFile.openStream()) { + XMLEventReader reader = parser.createXMLEventReader(is); + try { + int depth = 0; + while (reader.hasNext()) { + XMLEvent event = reader.peek(); + if (event.isStartElement()) { + if (depth == 0 && !RNG_GRAMMAR.equals(event.asStartElement().getName())) { + throw new IllegalArgumentException("File is not RNG: " + rngFile); + } else if (depth == 1 && RNG_INCLUDE.equals(event.asStartElement().getName())) { + Attribute href = event.asStartElement().getAttributeByName(_HREF); + if (href != null) { + URI uri = URI.create(href.getValue()); + if (uri.isAbsolute()) { + Module m = resolver.getModuleByComponent(uri); + if (m != null) { + Component component = m.getComponent(uri); + if (component == null) + throw new IllegalStateException("coding error"); // can not happen + dependencies.add(component); + } else { + throw new RuntimeException("" + rngFile + ": Couldn't resolve href " + uri); + } + } else { + // resolve relative path + dependencies.addAll( + listDependencies(URLs.resolve(URLs.asURI(rngFile), uri).toURL(), resolver, parser)); + } + } + } + depth++; + } else if (event.isEndElement()) { + depth--; + } + reader.next(); + } + } finally { + reader.close(); + } + } catch (IOException e) { + throw new RuntimeException("Couldn't access RelaxNG file " + rngFile, e); + } catch (XMLStreamException e) { + throw new RuntimeException("Couldn't parse RelaxNG file " + rngFile, e); + } + cache.put(rngFile, dependencies); + return dependencies; + } + } + + private final static Map> cache = new HashMap<>(); + +} diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/ResolutionException.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/ResolutionException.java new file mode 100644 index 0000000000..02cbf4fcfa --- /dev/null +++ b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/ResolutionException.java @@ -0,0 +1,23 @@ +package org.daisy.pipeline.modules; + +/** + * Exception thrown when a resource can not be resolved. + */ +public class ResolutionException extends RuntimeException { + + public ResolutionException() { + super(); + } + + public ResolutionException(String message) { + super(message); + } + + public ResolutionException(Throwable cause) { + super(cause); + } + + public ResolutionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/ResourceLoader.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/ResourceLoader.java index 8eb0babc12..bc5136b855 100644 --- a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/ResourceLoader.java +++ b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/ResourceLoader.java @@ -1,7 +1,7 @@ package org.daisy.pipeline.modules; import java.net.URL; - +import java.nio.file.NoSuchFileException; /** * The Interface ResourceLoader allows to get an accessible URI from the path provided. This is used for loading {@link Component} objects. @@ -11,15 +11,15 @@ public interface ResourceLoader { /** * Loads the resource. * - * @param path the path - * @return the uRL + * @param path the (not URL-encoded) path, relative to catalog.xml + * @return An encoded absolute URL + * @throws NoSuchFileException if the resource is not available */ - URL loadResource(String path); + URL loadResource(String path) throws NoSuchFileException; + /** * Loads a list of resources recursively. - * - * @param path the path - * @return the uRL */ Iterable loadResources(String path); + } diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/UseXSLTPackage.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/UseXSLTPackage.java new file mode 100644 index 0000000000..cd227685fe --- /dev/null +++ b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/UseXSLTPackage.java @@ -0,0 +1,27 @@ +package org.daisy.pipeline.modules; + +/** + * A {@code xsl:use-package} dependency. + */ +public class UseXSLTPackage implements Dependency { + + private final String name; + private final String version; + + public UseXSLTPackage(String name) { + this(name, "*"); + } + + public UseXSLTPackage(String name, String version) { + this.name = name; + this.version = version; + } + + public String getName() { + return name; + } + + public String getVersion() { + return version; + } +} diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/XProcResource.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/XProcResource.java new file mode 100644 index 0000000000..e0cc080e7b --- /dev/null +++ b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/XProcResource.java @@ -0,0 +1,174 @@ +package org.daisy.pipeline.modules; + +import java.io.File; +import java.io.InputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.nio.file.NoSuchFileException; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; + +import javax.xml.namespace.QName; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.StartElement; +import javax.xml.stream.events.XMLEvent; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLInputFactory; + +import org.daisy.common.file.URLs; + +public class XProcResource { + + private static final String XPROC_NS = "http://www.w3.org/ns/xproc"; + private static final String CALABASH_EX_NS = "http://xmlcalabash.com/ns/extensions"; + private static final QName P_LIBRARY = new QName(XPROC_NS, "library"); + private static final QName P_DECLARE_STEP = new QName(XPROC_NS, "declare-step"); + private static final QName P_PIPELINE = new QName(XPROC_NS, "pipeline"); + private static final QName P_IMPORT = new QName(XPROC_NS, "import"); + private static final QName P_DOCUMENT = new QName(XPROC_NS, "document"); + private static final QName CX_IMPORT = new QName(CALABASH_EX_NS, "import"); + private static final QName _HREF = new QName("href"); + private static final QName _TYPE = new QName("type"); + + private final URL resource; + private final Module module; + + public XProcResource(Module module, String path) throws NoSuchFileException { + this.resource = module.getResource(path); + this.module = module; + } + + public XProcResource(Component component) { + try { + this.resource = component.getResource().toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException("coding error"); + } + this.module = component.getModule(); + } + + /** + * List the dependencies of this resource. + * + * This recursively resolves all p:import, cx:import, + * p:document, xsl:import, xsl:include and + * rng:include that reference a component (either in the same module or in another + * one). It also resolves xsl:use-package and it resolves XPath extension functions + * defined in Java. Other dependencies are ignored. + * + * The dependencies may include (external) {@link Component}, {@link UseXSLTPackage} and + * (internal) {@link JavaDependency}. XSLT/XProc/RelaxNG resources from the same module are + * handled recursively. + */ + public Set listDependencies(ModuleRegistry resolver, Set sourceRoots, XMLInputFactory parser) { + return listDependencies(resource, true, false, module, resolver, sourceRoots, parser, new LinkedList<>()); + } + + /** + * @param file The resource. + * @param ensureXProc Assert that the resource is XProc. + * @param ensureXSLT Assert that the resource is XSLT. + * @param module The module that contains the resource. + * @param resolver The module registry, for resolving dependency URIs. + * @param sourceRoots Set of root directories containing source files. This is used to resolve + * Java source files when this method is called during compilation of the + * module. + * @param parser The StAX input factory for parsing the XML. + */ + private static Set listDependencies(URL file, boolean ensureXProc, boolean ensureXSLT, Module module, + ModuleRegistry resolver, Set sourceRoots, XMLInputFactory parser, + Deque stack) { + if (cache.containsKey(file)) { + return cache.get(file); + } else { + Set dependencies = new HashSet<>(); + if (stack.contains(file)) { + // cyclic dependencies within the same module are supported + return dependencies; + } + try (InputStream is = file.openStream()) { + XMLEventReader reader = parser.createXMLEventReader(is); + try { + int depth = 0; + while (reader.hasNext()) { + XMLEvent event = reader.peek(); + if (event.isStartElement()) { + StartElement elem = event.asStartElement(); + QName name = elem.getName(); + if (depth == 0) { + if (!(P_LIBRARY.equals(name) || P_DECLARE_STEP.equals(name) || P_PIPELINE.equals(name))) { + if (ensureXProc) + throw new IllegalArgumentException( + "File is not XProc: " + file + ": found root element " + name); + else if (XSLTResource.XSL_STYLESHEET.equals(name) || XSLTResource.XSL_PACKAGE.equals(name)) + return XSLTResource.listDependencies(file, module, resolver, sourceRoots, parser); + else if (ensureXSLT) + throw new IllegalArgumentException( + "File is not XSLT: " + file + ": found root element " + name); + else if (RelaxNGResource.RNG_GRAMMAR.equals(name)) + return RelaxNGResource.listDependencies(file, resolver, parser); + else + break; + } + } else if ((P_IMPORT.equals(name) || + (CX_IMPORT.equals(name) + && "type='application/xml+xslt'".equals("" + elem.getAttributeByName(_TYPE)))) || + P_DOCUMENT.equals(name)) { + Attribute href = elem.getAttributeByName(_HREF); + if (href == null) + throw new IllegalArgumentException( + "" + file + ": Invalid XProc: missing href attribute on " + name); + URI uri = URI.create(href.getValue()); + if (P_IMPORT.equals(name) + && "http://xmlcalabash.com/extension/steps/library-1.0.xpl".equals(uri.toString())) + ; // FIXME: create dependency on XMLCalabash + else if (uri.isAbsolute()) { + Module m = resolver.getModuleByComponent(uri); + if (m != null) { + Component component = m.getComponent(uri); + if (component == null) + throw new IllegalStateException("coding error"); // can not happen + dependencies.add(component); + } else { + throw new RuntimeException("" + file + ": Couldn't resolve href " + uri); + } + } else { + // resolve relative path + stack.push(file); + dependencies.addAll( + listDependencies(URLs.resolve(URLs.asURI(file), uri).toURL(), P_IMPORT.equals(name), + CX_IMPORT.equals(name), module, resolver, sourceRoots, parser, stack)); + stack.pop(); + } + } + depth++; + } else if (event.isEndElement()) { + depth--; + } + reader.next(); + } + } finally { + reader.close(); + } + } catch (IOException e) { + throw new RuntimeException("Couldn't access " + + (ensureXProc ? "XProc " : ensureXSLT ? "XSLT " : "") + "file " + file, e); + } catch (XMLStreamException e) { + throw new RuntimeException("Couldn't parse " + + (ensureXProc ? "XProc " : ensureXSLT ? "XSLT " : "") + "file " + file, e); + } + cache.put(file, dependencies); + return dependencies; + } + } + + private final static Map> cache = new HashMap<>(); + +} diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/XSLTPackage.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/XSLTPackage.java new file mode 100644 index 0000000000..be4375c612 --- /dev/null +++ b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/XSLTPackage.java @@ -0,0 +1,78 @@ +package org.daisy.pipeline.modules; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.NoSuchFileException; + +import javax.xml.namespace.QName; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.StartElement; +import javax.xml.stream.events.XMLEvent; + +public class XSLTPackage extends XSLTResource { + + private String name; + private String version; + + public XSLTPackage(Module module, String name, String version, String path) throws NoSuchFileException { + super(module, path); + this.name = name; + this.version = version != null + ? version + : module.getVersion().replaceAll("-SNAPSHOT$", ""); + } + + public XSLTPackage(Module module, String path, XMLInputFactory parser) throws NoSuchFileException, IllegalArgumentException { + super(module, path); + getPackageNameAndVersion(getResource(), parser); + if (version == null) + version = module.getVersion().replaceAll("-SNAPSHOT$", ""); + } + + public String getName() { + return name; + } + + public String getVersion() { + return version; + } + + private void getPackageNameAndVersion(URL xsltFile, XMLInputFactory parser) throws IllegalArgumentException { + try (InputStream is = xsltFile.openStream()) { + XMLEventReader reader = parser.createXMLEventReader(is); + try { + while (reader.hasNext()) { + XMLEvent event = reader.peek(); + if (event.isStartElement()) { + StartElement elem = event.asStartElement(); + QName elemName = elem.getName(); + if (!XSL_PACKAGE.equals(elemName)) + throw new IllegalArgumentException( + "File is not a XSLT package: " + xsltFile + ": found root element " + elemName); + Attribute name = elem.getAttributeByName(_NAME); + if (name == null) + throw new IllegalArgumentException( + "" + xsltFile + ": Invalid XSLT: missing name attribute on " + elemName); + this.name = name.getValue(); + Attribute version = elem.getAttributeByName(_PACKAGE_VERSION); + if (version != null) + this.version = version.getValue(); + return; + } + reader.next(); + } + throw new IllegalStateException("coding error"); // document without a root element: can not happen + } finally { + reader.close(); + } + } catch (IOException e) { + throw new RuntimeException("Couldn't access XSLT file " + xsltFile, e); + } catch (XMLStreamException e) { + throw new RuntimeException("Couldn't parse XSLT file " + xsltFile, e); + } + } +} diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/XSLTResource.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/XSLTResource.java new file mode 100644 index 0000000000..dd262868dd --- /dev/null +++ b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/XSLTResource.java @@ -0,0 +1,193 @@ +package org.daisy.pipeline.modules; + +import java.io.File; +import java.io.InputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.nio.file.NoSuchFileException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import javax.xml.namespace.QName; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.Namespace; +import javax.xml.stream.events.StartElement; +import javax.xml.stream.events.XMLEvent; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLInputFactory; + +import org.daisy.common.file.URLs; + +public class XSLTResource { + + private static final String XSLT_NS = "http://www.w3.org/1999/XSL/Transform"; + static final QName XSL_STYLESHEET = new QName(XSLT_NS, "stylesheet"); + static final QName XSL_PACKAGE = new QName(XSLT_NS, "package"); + private static final QName XSL_IMPORT = new QName(XSLT_NS, "import"); + private static final QName XSL_INCLUDE = new QName(XSLT_NS, "include"); + private static final QName XSL_USE_PACKAGE = new QName(XSLT_NS, "use-package"); + private static final QName _HREF = new QName("href"); + protected static final QName _NAME = new QName("name"); + protected static final QName _PACKAGE_VERSION = new QName("package-version"); + + private final URL resource; + private final Module module; + + public XSLTResource(Module module, String path) throws NoSuchFileException { + this.resource = module.getResource(path); + this.module = module; + } + + public XSLTResource(XSLTResource resource) { + this.resource = resource.resource; + this.module = resource.module; + } + + public XSLTResource(Component component) { + try { + this.resource = component.getResource().toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException("coding error"); + } + this.module = component.getModule(); + } + + public URL getResource() { + return resource; + } + + /** + * List the dependencies of this resource. + * + * This recursively resolves all xsl:import and xsl:include that + * reference a component (either in the same module or in another one). It also resolves + * xsl:use-package and it resolves XPath extension functions defined in Java. Other + * dependencies are ignored. + * + * The dependencies may include (external) {@link Component}, {@link UseXSLTPackage} and + * (internal) {@link JavaDependency}. Imported/included resources from the same module are + * handled recursively. + */ + public Set listDependencies(ModuleRegistry resolver, Set sourceRoots, XMLInputFactory parser) { + return listDependencies(resource, module, resolver, sourceRoots, parser); + } + + /** + * @param xsltFile The XSLT resource. + * @param module The module that contains the XSLT resource. + * @param resolver The module registry, for resolving dependency URIs. + * @param sourceRoots Set of root directories containing source files. This is used to resolve + * Java source files when this method is called during compilation of the + * module. + * @param parser The StAX input factory for parsing the XML. + */ + static Set listDependencies(URL xsltFile, Module module, ModuleRegistry resolver, Set sourceRoots, + XMLInputFactory parser) { + if (cache.containsKey(xsltFile)) { + return cache.get(xsltFile); + } else { + Set dependencies = new HashSet<>(); + try (InputStream is = xsltFile.openStream()) { + XMLEventReader reader = parser.createXMLEventReader(is); + try { + int depth = 0; + while (reader.hasNext()) { + XMLEvent event = reader.peek(); + if (event.isStartElement()) { + StartElement elem = event.asStartElement(); + QName elemName = elem.getName(); + if (depth == 0 && !(XSL_STYLESHEET.equals(elemName) || XSL_PACKAGE.equals(elemName))) { + throw new IllegalArgumentException( + "File is not XSLT: " + xsltFile + ": found root element " + elemName); + } else if (depth == 1 && (XSL_IMPORT.equals(elemName) || XSL_INCLUDE.equals(elemName))) { + Attribute href = elem.getAttributeByName(_HREF); + if (href == null) + throw new IllegalArgumentException( + "" + xsltFile + ": Invalid XSLT: missing href attribute on " + elemName); + URI uri = URI.create(href.getValue()); + if (uri.isAbsolute()) { + Module m = resolver.getModuleByComponent(uri); + if (m != null) { + Component component = m.getComponent(uri); + if (component == null) + throw new IllegalStateException("coding error"); // can not happen + dependencies.add(component); + } else { + throw new RuntimeException("" + xsltFile + ": Couldn't resolve href " + uri); + } + } else { + // resolve relative path + dependencies.addAll( + listDependencies(URLs.resolve(URLs.asURI(xsltFile), uri).toURL(), module, + resolver, sourceRoots, parser)); + } + } else if (depth == 1 && XSL_USE_PACKAGE.equals(elemName)) { + Attribute name = elem.getAttributeByName(_NAME); + if (name == null) + throw new IllegalArgumentException( + "" + xsltFile + ": Invalid XSLT: missing name attribute on " + elemName); + Attribute version = elem.getAttributeByName(_PACKAGE_VERSION); + dependencies.add( + version != null ? new UseXSLTPackage(name.getValue(), version.getValue()) + : new UseXSLTPackage(name.getValue())); + } + @SuppressWarnings("unchecked") + Iterator ns = elem.getNamespaces(); + while (ns.hasNext()) { + String nsUri = ns.next().getNamespaceURI(); + // In addition to checking that the namespace has the form of a + // fully qualified Java class name, use the heuristic that + // ExtensionFunctionProvider classes are not in the default package. + if (FQCN.matcher(nsUri).matches() && nsUri.contains(".")) { + String className = nsUri; + // assuming it is the namespace of a XPath extension function, and the function + // is used within this element + String javaSourceFile = className.split("\\$")[0].replace('.', '/') + ".java"; + boolean internal = false; { + if (sourceRoots != null) + for (File root : sourceRoots) + if (new File(root, javaSourceFile).exists()) { + // Java source file is inside the module we are compiling + internal = true; + break; }} + if (!internal) { + // require that the function is implemented in the same module (and exposed + // through a public XSLT to other modules) + throw new RuntimeException( + "" + xsltFile + ": Java dependency could not be resolved: " + className + + " (expected to be in the same module)"); + } + dependencies.add(new JavaDependency(module, className)); + } + } + depth++; + } else if (event.isEndElement()) { + depth--; + } + reader.next(); + } + } finally { + reader.close(); + } + } catch (IOException e) { + throw new RuntimeException("Couldn't access XSLT file " + xsltFile, e); + } catch (XMLStreamException e) { + throw new RuntimeException("Couldn't parse XSLT file " + xsltFile, e); + } + cache.put(xsltFile, dependencies); + return dependencies; + } + } + + private final static Map> cache = new HashMap<>(); + + private final static Pattern FQCN = Pattern.compile("([a-zA-Z_$][a-zA-Z\\d_$]*\\.)*[a-zA-Z_$][a-zA-Z\\d_$]*"); + +} diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/DefaultModuleRegistry.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/DefaultModuleRegistry.java new file mode 100644 index 0000000000..23e042bde4 --- /dev/null +++ b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/DefaultModuleRegistry.java @@ -0,0 +1,441 @@ +package org.daisy.pipeline.modules.impl; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; + +import com.google.common.collect.Lists; + +import org.daisy.common.spi.ServiceLoader; +import org.daisy.pipeline.modules.Component; +import org.daisy.pipeline.modules.Entity; +import org.daisy.pipeline.modules.Module; +import org.daisy.pipeline.modules.ModuleRegistry; +import org.daisy.pipeline.modules.ResolutionException; +import org.daisy.pipeline.modules.XSLTPackage; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.Version; +import org.osgi.framework.VersionRange; + +@org.osgi.service.component.annotations.Component( + name = "module-registry", + service = { ModuleRegistry.class } +) +public class DefaultModuleRegistry implements ModuleRegistry { + + private static final Logger logger = LoggerFactory.getLogger(DefaultModuleRegistry.class); + + private final HashMap componentsMap = new HashMap<>(); + private final HashMap entityMap = new HashMap<>(); + private final HashMap xsltPackageMap = new HashMap<>(); + private final HashMap codeSourceLocationMap = new HashMap<>(); + private final List modules = new ArrayList<>(); + private Iterator nextModules; + private final LinkedList modulesBeingInitialized = new LinkedList<>(); + /* + * Note that thanks to SaxonConfigurator.setModuleRegistry(), which calls + * ModuleRegistry.iterator(), the module registry and all modules will be fully initialized + * before the web service goes up. + */ + private boolean initialized = false; + private int detectEndlessRecursion = 0; + + public DefaultModuleRegistry() { + if (OSGiHelper.inOSGiContext()) + nextModules = OSGiHelper.getModules(); + else + nextModules = SPIHelper.getModules(); + } + + private Set components; + private Set entities; + + /** + * Whether some module satisfies {@code module.hasComponent(uri)}. + */ + private boolean hasComponent(URI uri) { + if (components == null) { + components = new HashSet<>(); + entities = new HashSet<>(); + for (Module m : modules) { + components.addAll(m.getComponents()); + entities.addAll(m.getEntities()); + } + for (Module m : modulesBeingInitialized) { + components.addAll(m.getComponents()); + entities.addAll(m.getEntities()); + } + if (nextModules.hasNext()) { + // load modules only after DefaultModuleRegistry has been created, + // because some module activator methods may use DefaultModuleRegistry + List modules = Lists.newArrayList(nextModules); + for (Module m : modules) { + components.addAll(m.getComponents()); + entities.addAll(m.getEntities()); + } + nextModules = modules.iterator(); + } + } + return components.contains(uri); + } + + /** + * Whether some module satisfies {@code module.hasEntity(uri)}. + */ + private boolean hasEntity(String publicId) { + if (entities == null) + hasComponent(null); // initialize entities + return entities.contains(publicId); + } + + private void addNextModule() throws NoSuchElementException { + if (initialized) + throw new NoSuchElementException(); + do { + Module module; { + if (nextModules.hasNext()) + module = nextModules.next(); + else if (!modulesBeingInitialized.isEmpty()) { + if (detectEndlessRecursion > 2 * modulesBeingInitialized.size()) + // An endless recursion is detected. This can happen either because a + // dependency is missing, or because there is a circular dependency. We + // assume that the recursive call comes from getModuleByComponent(), + // getModuleByEntity(), getModuleByXSLTPackage() or getModuleByClass(). All + // of these methods first check that the component/entity/package/class in + // question is provided by one the modules (with unresolved dependencies), + // before calling addNextModule(). So we can say that we've detected a + // circular dependency. + throw new RuntimeException("Circular dependency detected"); + module = modulesBeingInitialized.poll(); + } else { + initialized = true; + throw new NoSuchElementException(); + } + } + detectEndlessRecursion++; + modulesBeingInitialized.add(module); + try { + // Resolve dependencies of the module. We do it here in ModuleRegistry so that a + // module does not need to do it during construction. This allows module components + // to depend on components of other modules. Because modules that make use of + // ModuleRegistry may recursively call their own resolveDependencies() method, a + // module may already be partly initialized when module.resolveDependencies() is + // called. + module.resolveDependencies(); + } catch (Throwable e) { + logger.warn("An error happened while resolving dependencies of module " + module, e); + continue; + } finally { + modulesBeingInitialized.remove(module); + } + if (modules.contains(module)) + // already added in a recursive call + return; + detectEndlessRecursion = 0; + logger.debug("Adding module {}", module); + modules.add(module); + for (URI component : module.getComponents()) { + try { + module.getComponent(component); + logger.debug(" - {}", component); + componentsMap.put(component, module); + } catch (ResolutionException e) { + // ignore components that can not be resolved + } + } + for (String entity: module.getEntities()) { + try { + module.getEntity(entity); + logger.debug(" - {}", entity); + entityMap.put(entity, module); + } catch (ResolutionException e) { + // ignore entities that can not be resolved + } + } + for (XSLTPackage pack : module.getXSLTPackages()) { + xsltPackageMap.put(pack.getName(), module); + } + URL codeSourceLocation = getCodeSourceLocation(module.getClass()); + if (codeSourceLocationMap.containsKey(codeSourceLocation)) + throw new IllegalStateException("Not more than one module is allowed in one bundle"); + codeSourceLocationMap.put(codeSourceLocation, module); + logger.trace("Added module: " + + module.getClass().getName() + + "@" + Integer.toHexString(System.identityHashCode(module))); + return; + } while (true); + } + + /** + * {@link Module} objects that call this method should be aware that this may call their own + * {@link Module#resolveDependencies} method. + */ + @Override + public synchronized Iterator iterator() { + if (initialized) + return modules.iterator(); + return new Iterator() { + private int index = 0; + public boolean hasNext() { + synchronized (DefaultModuleRegistry.this) { + if (index < modules.size()) + return true; + return nextModules.hasNext() || !modulesBeingInitialized.isEmpty(); + } + } + public Module next() throws NoSuchElementException { + synchronized (DefaultModuleRegistry.this) { + if (index >= modules.size()) + addNextModule(); + return modules.get(index++); + } + } + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + /** + * {@link Module} objects that call this method should be aware that this may call their own + * {@link Module#resolveDependencies} method. + */ + @Override + public synchronized Module getModuleByComponent(URI uri) { + if (!hasComponent(uri)) + return null; + do { + Module module = componentsMap.get(uri); + if (module != null) + return module; + for (Module m : modules) + if (m.hasComponent(uri)) + // This means that a module provides the component, but + // the component could not be resolved. It's no use + // checking other modules. + try { + // Just to be sure check whether the component can + // really not be resolved. This should result in a + // ResolutionException. + m.getComponent(uri); + return m; + } catch (ResolutionException e) { + return null; + } + try { + addNextModule(); + } catch (NoSuchElementException e) { + break; + } + } while (true); + return null; + } + + /** + * {@link Module} objects that call this method should be aware that this may call their own + * {@link Module#resolveDependencies} method. + */ + @Override + public synchronized Module getModuleByComponent(URI uri, String versionRange) { + Module module = getModuleByComponent(uri); + if (module != null && versionRange != null) { + Component component = module.getComponent(uri); + if (component == null) + throw new IllegalStateException("coding error"); // can not happen + Version version; { + try { + version = Version.parseVersion(component.getVersion()); + } catch (IllegalArgumentException e) { + logger.debug("Version of " + uri + " can not be parsed: " + component.getVersion()); + return null; + } + } + try { + if (!(new VersionRange(versionRange).includes(version))) { + logger.debug("Version of " + uri + " (" + version + ") not in requested range (" + versionRange + ")"); + return null; + } + } catch (IllegalArgumentException e) { + logger.debug("Version range can not be parsed: " + versionRange); + return null; + } + } + return module; + } + + /** + * Returns only the components that could be resolved (unlike {@link Module#getComponents}). + * + * {@link Module} objects that call this method should be aware that this may call their own + * {@link Module#resolveDependencies} method. + */ + @Override + public synchronized Iterable getComponents() { + if (!initialized) + while (true) + try { + addNextModule(); + } catch (NoSuchElementException e) { + break; + } + return componentsMap.keySet(); + } + + /** + * {@link Module} objects that call this method should be aware that this may call their own + * {@link Module#resolveDependencies} method. + */ + @Override + public synchronized Module getModuleByEntity(String publicId) { + if (!hasEntity(publicId)) + return null; + do { + Module module = entityMap.get(publicId); + if (module != null) + return module; + for (Module m : modules) + if (m.hasEntity(publicId)) + // This means that a module provides the entity, but the + // entity could not be resolved. It's no use checking + // other modules. + try { + // Just to be sure check whether the entity can + // really not be resolved. This should result in a + // ResolutionException. + m.getEntity(publicId); + return m; + } catch (ResolutionException e) { + return null; + } + try { + addNextModule(); + } catch (NoSuchElementException e) { + break; + } + } while (true); + return null; + } + + /** + * Returns only the entities that could be resolved (unlike {@link Module#getEntities}). + * + * {@link Module} objects that call this method should be aware that this may call their own + * {@link Module#resolveDependencies} method. + */ + @Override + public synchronized Iterable getEntities() { + if (!initialized) + while (true) + try { + addNextModule(); + } catch (NoSuchElementException e) { + break; + } + return entityMap.keySet(); + } + + /** + * {@link Module} objects that call this method should be aware that this may call their own + * {@link Module#resolveDependencies} method. + */ + @Override + public synchronized Module getModuleByXSLTPackage(String name) { + do { + Module module = xsltPackageMap.get(name); + if (module != null) + return module; + try { + addNextModule(); + } catch (NoSuchElementException e) { + break; + } + } while (true); + return null; + } + + /** + * {@link Module} objects that call this method should be aware that this may call their own + * {@link Module#resolveDependencies} method. + */ + @Override + public synchronized Module getModuleByClass(Class clazz) { + URL location = getCodeSourceLocation(clazz); + do { + Module module = codeSourceLocationMap.get(location); + if (module != null) + return module; + try { + addNextModule(); + } catch (NoSuchElementException e) { + break; + } + } while (true); + return null; + } + + private static URL getCodeSourceLocation(Class clazz) throws IllegalArgumentException { + URL location = clazz.getProtectionDomain().getCodeSource().getLocation(); + if (!(location.toString().startsWith("file:") || location.toString().startsWith("bundle:") || location.toString().startsWith("mvn:"))) + throw new RuntimeException("unexpected code source location: " + location); + return location; + } + + // static nested class in order to delay class loading + private static abstract class OSGiHelper { + + static boolean inOSGiContext() { + try { + return FrameworkUtil.getBundle(OSGiHelper.class) != null; + } catch (NoClassDefFoundError e) { + return false; + } + } + + static Iterator getModules() { + return new Iterator() { + private BundleContext bc = FrameworkUtil.getBundle(DefaultModuleRegistry.class).getBundleContext(); + private ServiceReference[] refs; + private int index = 0; { + try { + refs = bc.getServiceReferences(Module.class.getName(), null); + } catch (InvalidSyntaxException e) { + throw new IllegalStateException(e); // should not happen + } + } + public boolean hasNext() { + return refs != null && index < refs.length; + } + public Module next() throws NoSuchElementException { + if (!hasNext()) + throw new NoSuchElementException(); + return (Module)bc.getService(refs[index++]); + } + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + } + + // static nested class in order to delay class loading + private static abstract class SPIHelper { + static Iterator getModules() { + return ServiceLoader.load(Module.class).iterator(); + } + } +} diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/resolver/ModuleUriResolver.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/ModuleUriResolver.java similarity index 71% rename from framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/resolver/ModuleUriResolver.java rename to framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/ModuleUriResolver.java index 49df686545..04cda54add 100644 --- a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/resolver/ModuleUriResolver.java +++ b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/ModuleUriResolver.java @@ -1,9 +1,10 @@ -package org.daisy.pipeline.modules.impl.resolver; +package org.daisy.pipeline.modules.impl; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URI; +import java.util.NoSuchElementException; import javax.xml.transform.Source; import javax.xml.transform.URIResolver; @@ -11,8 +12,11 @@ import org.daisy.pipeline.modules.Module; import org.daisy.pipeline.modules.ModuleRegistry; +import org.daisy.pipeline.modules.ResolutionException; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -97,13 +101,16 @@ public Source resolve(String href, String base) { private Source resolveFromModules(URI href) { Module mod = mRegistry.getModuleByComponent(href); if (mod == null) { - mLogger.trace("No module found for uri:" + href); + mLogger.trace("No module found for URI: " + href); return null; } - URI resource = mod.getComponent(href).getResource(); - if (resource == null) { - mLogger.trace("No resource found in module " + mod.getName() + " for uri :" + href); - return null; + mLogger.trace(href.toString() + " found in module " + mod.getName()); + URI resource; { + try { + resource = mod.getComponent(href).getResource(); + } catch (NoSuchElementException|ResolutionException e) { + throw new IllegalStateException("coding error"); + } } SAXSource source = new SAXSource(new InputSource(resource.toString())); return source; @@ -116,26 +123,30 @@ private Source resolveFromModules(URI href) { * java.lang.String) */ @Override - public InputSource resolveEntity(String publicId, String systemId) - throws SAXException, IOException { - InputSource src=null; + public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { Module mod = mRegistry.getModuleByEntity(publicId); - //by public id - if (mod != null &&mod.getEntity(publicId)!=null && mod.getEntity(publicId).getResource()!=null) { - src=new InputSource(mod.getEntity(publicId).getResource().toString()); - - }else{ - //by systemId - mLogger.trace("No module found for publicId:" + publicId); - URI uriSystem=URI.create(systemId); - mod = mRegistry.getModuleByComponent(uriSystem); - if (mod != null) { - src=new InputSource(mod.getComponent(uriSystem).getResource().toString()); - }else{ - mLogger.trace("No module found for uri:" + systemId); + // by public id + if (mod != null) { + mLogger.trace("Entity " + publicId + " found in module " + mod.getName()); + URI resource; { + try { + resource = mod.getEntity(publicId).getResource(); + } catch (NoSuchElementException|ResolutionException e) { + throw new IllegalStateException("coding error"); + } } + return new InputSource(resource.toString()); + } + // by systemId + mLogger.trace("No module found for publicId:" + publicId); + URI uriSystem=URI.create(systemId); + mod = mRegistry.getModuleByComponent(uriSystem); + if (mod != null) { + return new InputSource(mod.getComponent(uriSystem).getResource().toString()); + } else { + mLogger.trace("No module found for publicId: " + systemId); + return null; } - return src; } } diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/resolver/package-info.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/resolver/package-info.java deleted file mode 100644 index b31a2b8af0..0000000000 --- a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/resolver/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * URI resolver which uses the tracked modules' components to translate a public URI to a Source object - */ -package org.daisy.pipeline.modules.impl.resolver; diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/tracker/DefaultModuleRegistry.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/tracker/DefaultModuleRegistry.java deleted file mode 100644 index 452fb1de45..0000000000 --- a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/tracker/DefaultModuleRegistry.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.daisy.pipeline.modules.impl.tracker; - -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; - -import org.daisy.pipeline.modules.Component; -import org.daisy.pipeline.modules.Entity; -import org.daisy.pipeline.modules.Module; -import org.daisy.pipeline.modules.ModuleRegistry; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Deactivate; -import org.osgi.service.component.annotations.Reference; -import org.osgi.service.component.annotations.ReferenceCardinality; -import org.osgi.service.component.annotations.ReferencePolicy; - -@org.osgi.service.component.annotations.Component( - name = "module-registry", - service = { ModuleRegistry.class } -) -public class DefaultModuleRegistry implements ModuleRegistry { - - private static final Logger logger = LoggerFactory - .getLogger(DefaultModuleRegistry.class); - - private final HashMap componentsMap = new HashMap(); - private final HashMap entityMap = new HashMap(); - private final HashSet modules = new HashSet(); - - public DefaultModuleRegistry() { - } - - @Activate - public void init() { - logger.trace("Module registry up"); - } - - @Deactivate - public void close() { - } - - @Override - public Iterator iterator() { - return modules.iterator(); - } - - @Override - public Module getModuleByComponent(URI uri) { - return componentsMap.get(uri); - } - - @Override - public Module resolveDependency(URI component, Module source) { - // TODO check cache, otherwise delegate to resolver - return null; - } - - @Override - public Iterable getComponents() { - return componentsMap.keySet(); - } - - @Reference( - name = "Module", - unbind = "-", - service = Module.class, - cardinality = ReferenceCardinality.MULTIPLE, - policy = ReferencePolicy.STATIC - ) - public void addModule(Module module) { - logger.debug("Registring module {}", module.getName()); - modules.add(module); - for (Component component : module.getComponents()) { - logger.debug(" - {}", component.getURI()); - componentsMap.put(component.getURI(), module); - } - for (Entity entity: module.getEntities()) { - logger.debug(" - {}", entity.getPublicId()); - entityMap.put(entity.getPublicId(), module); - } - } - - public void removeModule(Module module) { - // FIXME: remove module - } - - @Override - public Module getModuleByEntity(String publicId) { - return entityMap.get(publicId); - } - - @Override - public Iterable getEntities() { - return entityMap.keySet(); - } - - -} diff --git a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/tracker/package-info.java b/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/tracker/package-info.java deleted file mode 100644 index c22648a164..0000000000 --- a/framework/modules-registry/src/main/java/org/daisy/pipeline/modules/impl/tracker/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Module tracking used to manage any module loaded into the framework. - */ -package org.daisy.pipeline.modules.impl.tracker; diff --git a/framework/modules-registry/src/test/java/org/daisy/pipeline/modules/ModuleTest.java b/framework/modules-registry/src/test/java/org/daisy/pipeline/modules/ModuleTest.java index 7946314ec1..364fec6710 100644 --- a/framework/modules-registry/src/test/java/org/daisy/pipeline/modules/ModuleTest.java +++ b/framework/modules-registry/src/test/java/org/daisy/pipeline/modules/ModuleTest.java @@ -4,11 +4,11 @@ import java.net.MalformedURLException; import java.net.URI; import java.net.URL; +import java.nio.file.NoSuchFileException; import java.util.Iterator; import javax.xml.stream.XMLInputFactory; -import org.daisy.pipeline.modules.Component; import org.daisy.pipeline.modules.Module; import org.daisy.pipeline.modules.ResourceLoader; import org.daisy.pipeline.xmlcatalog.impl.StaxXmlCatalogParser; @@ -22,7 +22,7 @@ public class ModuleTest { @Test - public void testBuildModule() { + public void testBuildModule() throws NoSuchFileException { StaxXmlCatalogParser catalogParser = new StaxXmlCatalogParser(); catalogParser.setFactory(XMLInputFactory.newInstance()); catalogParser.activate(); @@ -42,12 +42,15 @@ public Iterable loadResources(String path) { throw new UnsupportedOperationException("not implemented"); } }; - Module module = new Module("name", "version", "title") {}; - module.parseCatalog(catalog, resourceLoader); - Iterator components = module.getComponents().iterator(); + Module module = new Module("name", "version", "title", resourceLoader) { + @Override + public void resolveDependencies() {} + }; + Module.parseCatalog(module, catalog); + Iterator components = module.getComponents().iterator(); assertTrue(components.hasNext()); - Component c = components.next(); - assertEquals("http://example-module/hello.xml", c.getURI().toString()); + URI c = components.next(); + assertEquals("http://example-module/hello.xml", c.toString()); assertFalse(components.hasNext()); } } diff --git a/framework/parent/pom.xml b/framework/parent/pom.xml index 4023c91ba6..9adff035f4 100644 --- a/framework/parent/pom.xml +++ b/framework/parent/pom.xml @@ -158,7 +158,7 @@ org.daisy.pipeline ds-to-spi-runtime - 1.1.0 + 1.2.1-SNAPSHOT org.daisy.pipeline.build diff --git a/framework/saxon-adapter/pom.xml b/framework/saxon-adapter/pom.xml index ffa92a2ffa..ee1f51f72d 100644 --- a/framework/saxon-adapter/pom.xml +++ b/framework/saxon-adapter/pom.xml @@ -11,7 +11,7 @@ saxon-adapter - 5.0.1-SNAPSHOT + 5.5.0-SNAPSHOT bundle DAISY Pipeline 2 :: Adapter for Saxon @@ -21,6 +21,10 @@ org.daisy.pipeline common-utils + + org.daisy.pipeline + modules-registry + com.google.guava guava @@ -43,14 +47,6 @@ ds-to-spi-runtime provided - - - org.daisy.pipeline - modules-registry - runtime - diff --git a/framework/saxon-adapter/src/main/java/org/daisy/common/saxon/SaxonConfigurator.java b/framework/saxon-adapter/src/main/java/org/daisy/common/saxon/SaxonConfigurator.java index d888d324de..52cb7ee0f7 100644 --- a/framework/saxon-adapter/src/main/java/org/daisy/common/saxon/SaxonConfigurator.java +++ b/framework/saxon-adapter/src/main/java/org/daisy/common/saxon/SaxonConfigurator.java @@ -2,12 +2,16 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamSource; import javax.xml.transform.URIResolver; import net.sf.saxon.Configuration; @@ -22,6 +26,9 @@ import net.sf.saxon.xpath.XPathFactoryImpl; import org.daisy.common.xpath.saxon.XPathFunctionRegistry; +import org.daisy.pipeline.modules.Module; +import org.daisy.pipeline.modules.ModuleRegistry; +import org.daisy.pipeline.modules.XSLTPackage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -144,6 +151,32 @@ public void addPackageDetails(PackageDetails pack) { packages.add(pack); } + @Reference( + name = "ModuleRegistry", + unbind = "-", + service = ModuleRegistry.class, + cardinality = ReferenceCardinality.OPTIONAL, + policy = ReferencePolicy.STATIC + ) + public void setModuleRegistry(ModuleRegistry moduleRegistry) { + for (Module module : moduleRegistry) { + for (XSLTPackage pack : module.getXSLTPackages()) { + addPackageDetails( + new PackageDetails() {{ + try { + nameAndVersion = new VersionedPackageName(pack.getName(), pack.getVersion()); + URL url = pack.getResource(); + sourceLocation = new StreamSource(url.openStream(), url.toURI().toASCIIString()); + } catch (XPathException|IOException|URISyntaxException e) { + throw new IllegalStateException( + "Failed to create PackageDetails object for package '" + pack.getName() + "'", e); + } + }} + ); + } + } + } + public void removePackageDetails(PackageDetails pack) { if (packages != null) packages.remove(pack); From a2a8b48626a3e512665b27ed11ffabcd53c7f9f4 Mon Sep 17 00:00:00 2001 From: Bert Frees Date: Mon, 11 Nov 2024 20:05:48 +0100 Subject: [PATCH 02/15] git subrepo pull utils/build-utils subrepo: subdir: "utils/build-utils" merged: "9ba4efe415" upstream: origin: "git@github.com:daisy/pipeline-build-utils.git" branch: "master" commit: "7111f4b73a" git-subrepo: version: "0.3.1" origin: "???" commit: "???" --- utils/build-utils/.gitrepo | 4 +- .../org/daisy/common/spi/ServiceLoader.java | 61 +- .../build-utils/modules-build-helper/pom.xml | 45 +- .../src/it/it-pipeline-module/pom.xml | 27 +- .../src/it/xprocspec/test.xprocspec | 37 +- .../src/main/resources/META-INF/catalog.xml | 4 +- .../src/main/resources/liblouis/foo.utb | 2 + .../src/test/java/ServicesTest.java | 23 +- .../plugin/CalabashWithPipelineModules.java | 22 +- .../GenerateModuleClassFunctionProvider.java | 696 ++++++++++++++++++ .../maven/plugin/HtmlizeSourcesMojo.java | 5 +- .../maven/plugin/ProcessCatalogMojo.java | 18 +- .../maven/plugin/ProcessTestCatalogMojo.java | 18 +- .../daisy/pipeline/maven/plugin/utils.java | 15 + .../process-catalog/process-catalog.xsl | 136 +--- utils/build-utils/modules-test-helper/pom.xml | 6 +- 16 files changed, 872 insertions(+), 247 deletions(-) create mode 100644 utils/build-utils/modules-build-helper/src/it/it-pipeline-module/src/main/resources/liblouis/foo.utb create mode 100644 utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/GenerateModuleClassFunctionProvider.java diff --git a/utils/build-utils/.gitrepo b/utils/build-utils/.gitrepo index 9654863e44..42a04b108a 100644 --- a/utils/build-utils/.gitrepo +++ b/utils/build-utils/.gitrepo @@ -6,6 +6,6 @@ [subrepo] remote = git@github.com:daisy/pipeline-build-utils.git branch = master - commit = 62ec52c0fa21f667b586594e3ecef0ee8a8635fd - parent = 99c183f1d5116eb8c694fdd532cf21935ebe928a + commit = 7111f4b73a29a34798d175b7c0998c722e02e6f8 + parent = fc27d04c4788948cce4b9d43ac78d895376c24e2 cmdver = 0.3.1 diff --git a/utils/build-utils/ds-to-spi/ds-to-spi-runtime/src/main/java/org/daisy/common/spi/ServiceLoader.java b/utils/build-utils/ds-to-spi/ds-to-spi-runtime/src/main/java/org/daisy/common/spi/ServiceLoader.java index 17c3013d0b..40cd360b9f 100644 --- a/utils/build-utils/ds-to-spi/ds-to-spi-runtime/src/main/java/org/daisy/common/spi/ServiceLoader.java +++ b/utils/build-utils/ds-to-spi/ds-to-spi-runtime/src/main/java/org/daisy/common/spi/ServiceLoader.java @@ -22,8 +22,8 @@ public class ServiceLoader implements Iterable { private static ClassLoader lastContextClassLoader = null; public static ServiceLoader load(Class serviceType) { - ClassLoader ccl = Thread.currentThread().getContextClassLoader(); - if (ccl != lastContextClassLoader) { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader != lastContextClassLoader) { cache.clear(); synchronized (Memoize.singletons) { for (Object o : Memoize.singletons.values()) @@ -34,53 +34,62 @@ public static ServiceLoader load(Class serviceType) { Memoize.singletons.clear(); } } - lastContextClassLoader = ccl; + lastContextClassLoader = classLoader; ServiceLoader loader; if (cache.containsKey(serviceType)) { loader = (ServiceLoader)cache.get(serviceType); } else { - loader = new ServiceLoader(serviceType); + loader = new ServiceLoader(serviceType, classLoader); cache.put(serviceType, loader); } return loader; } - private Class serviceType; + private final Class serviceType; private Iterable serviceLoader; + private final ClassLoader classLoader; - private ServiceLoader(Class serviceType) { + private ServiceLoader(Class serviceType, ClassLoader classLoader) { this.serviceType = serviceType; + this.classLoader = classLoader; } public Iterator iterator() { return new AbstractIterator() { Iterator serviceIterator; public S computeNext() { - if (serviceIterator == null) { - try { - if (serviceLoader == null) { - serviceLoader = memoize(serviceType, java.util.ServiceLoader.load(serviceType)); + ClassLoader restoreClassLoader = Thread.currentThread().getContextClassLoader(); + if (classLoader != restoreClassLoader) + Thread.currentThread().setContextClassLoader(classLoader); + try { + if (serviceIterator == null) { + try { + if (serviceLoader == null) + serviceLoader = memoize(serviceType, java.util.ServiceLoader.load(serviceType)); + serviceIterator = serviceLoader.iterator(); + } catch (Throwable e) { + logger.error("Failed to load service providers", e); + return endOfData(); } - serviceIterator = serviceLoader.iterator(); - } catch (Throwable e) { - logger.error("Failed to load service providers", e); - return endOfData(); } - } - while (serviceIterator.hasNext()) { - try { - return serviceIterator.next(); - } catch (Throwable e) { - if (e instanceof ServiceConfigurationError && e.getCause() instanceof ActivationException) { - logger.info(e.getMessage() + ": " + e.getCause().getMessage()); - if (e.getCause().getCause() != null) - logger.trace("Cause:", e.getCause().getCause()); - } else { - logger.error("Failed to instantiate provider of service '" + serviceType.getCanonicalName() + "'", e); + while (serviceIterator.hasNext()) { + try { + return serviceIterator.next(); + } catch (Throwable e) { + if (e instanceof ServiceConfigurationError && e.getCause() instanceof ActivationException) { + logger.info(e.getMessage() + ": " + e.getCause().getMessage()); + if (e.getCause().getCause() != null) + logger.trace("Cause:", e.getCause().getCause()); + } else { + logger.error("Failed to instantiate provider of service '" + serviceType.getCanonicalName() + "'", e); + } } } + return endOfData(); + } finally { + if (classLoader != restoreClassLoader) + Thread.currentThread().setContextClassLoader(restoreClassLoader); } - return endOfData(); } }; } diff --git a/utils/build-utils/modules-build-helper/pom.xml b/utils/build-utils/modules-build-helper/pom.xml index c038c6b6b8..27c3df05b6 100644 --- a/utils/build-utils/modules-build-helper/pom.xml +++ b/utils/build-utils/modules-build-helper/pom.xml @@ -12,7 +12,7 @@ org.daisy.pipeline.build modules-build-helper - 2.8.1-SNAPSHOT + 3.0.0-SNAPSHOT maven-plugin DAISY Pipeline 2 :: Modules Build Helper @@ -20,12 +20,12 @@ 1.1.3 - 1.2.0 + 1.2.1-SNAPSHOT 1.4.2 - 1.14.16 - 1.14.20 - 2.2.4 - 1.14.6 + 1.14.21-SNAPSHOT + 1.14.26 + + 1.14.7 @@ -55,7 +55,7 @@ org.daisy.maven xproc-engine-calabash - 1.1.4 + 1.2.0-SNAPSHOT org.slf4j @@ -65,6 +65,10 @@ org.daisy.pipeline modules-registry + + org.daisy.pipeline + saxon-adapter + org.codehaus.woodstox woodstox-core-lgpl @@ -119,11 +123,6 @@ pom test - - org.daisy.pipeline - saxon-adapter - test - org.daisy.pipeline calabash-adapter @@ -133,7 +132,7 @@ org.daisy.pipeline framework-core test - + org.daisy.pipeline.build ds-to-spi-maven-plugin @@ -155,13 +154,13 @@ xprocspec ${xprocspec.version} test - + org.daisy.maven xproc-engine-daisy-pipeline @@ -172,6 +171,14 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + maven-plugin-plugin 3.5 @@ -188,7 +195,7 @@ @{project.artifactId}-@{project.version} - + + org.daisy.pipeline:framework-bom:${framework.version}:pom org.daisy.xprocspec:xprocspec:${xprocspec.version} org.daisy.pipeline:ds-to-spi-runtime:${ds-to-spi-runtime.version} @@ -233,7 +240,7 @@ - + --> diff --git a/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/pom.xml b/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/pom.xml index 70e4148bfb..cacccd316d 100644 --- a/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/pom.xml +++ b/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/pom.xml @@ -12,7 +12,7 @@ org.daisy.pipeline.modules modules-parent - 1.14.20 + 1.14.26 org.daisy.pipeline.build.modules-build-helper @@ -28,6 +28,11 @@ pom import + + org.daisy.pipeline + logging-activator + 2.1.0 + org.daisy.pipeline.modules modules-bom @@ -35,12 +40,6 @@ pom import - - - net.java.dev.jna - jna - 5.14.0 - @@ -59,15 +58,21 @@ org.daisy.pipeline framework-core + org.daisy.pipeline ds-to-spi-runtime @@ -80,11 +85,6 @@ 4.3.1 provided - - org.daisy.pipeline - logging-appender - test - junit junit @@ -276,7 +276,8 @@ maven-surefire-plugin - PaxExam,OSGiLessRunner + + OSGiLessRunner diff --git a/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/src/it/xprocspec/test.xprocspec b/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/src/it/xprocspec/test.xprocspec index 936da446d7..80f1ebb972 100644 --- a/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/src/it/xprocspec/test.xprocspec +++ b/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/src/it/xprocspec/test.xprocspec @@ -27,23 +27,21 @@ - - + - - + @@ -67,17 +65,16 @@ + - - - - + + @@ -260,7 +257,7 @@ - + @@ -294,10 +291,6 @@ - - - - @@ -312,10 +305,10 @@ - - - - + + + + @@ -455,10 +448,10 @@ - - - - + + + + diff --git a/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/src/main/resources/META-INF/catalog.xml b/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/src/main/resources/META-INF/catalog.xml index 97f3c69224..00bf238c83 100644 --- a/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/src/main/resources/META-INF/catalog.xml +++ b/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/src/main/resources/META-INF/catalog.xml @@ -12,7 +12,7 @@ ../css/foo-2.xpl"/> - + + diff --git a/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/src/main/resources/liblouis/foo.utb b/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/src/main/resources/liblouis/foo.utb new file mode 100644 index 0000000000..2888443896 --- /dev/null +++ b/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/src/main/resources/liblouis/foo.utb @@ -0,0 +1,2 @@ +letter f 124 +letter o 135 diff --git a/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/src/test/java/ServicesTest.java b/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/src/test/java/ServicesTest.java index cdafe6b848..bec9b5271c 100644 --- a/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/src/test/java/ServicesTest.java +++ b/utils/build-utils/modules-build-helper/src/it/it-pipeline-module/src/test/java/ServicesTest.java @@ -7,17 +7,11 @@ import org.daisy.pipeline.script.XProcScriptService; import org.daisy.pipeline.junit.AbstractXSpecAndXProcSpecTest; -import static org.daisy.pipeline.pax.exam.Options.mavenBundle; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import org.junit.Test; -import org.ops4j.pax.exam.Configuration; -import static org.ops4j.pax.exam.CoreOptions.composite; -import static org.ops4j.pax.exam.CoreOptions.options; -import org.ops4j.pax.exam.Option; - public class ServicesTest extends AbstractXSpecAndXProcSpecTest { @Inject @@ -39,8 +33,11 @@ public void testDatatype() { assertTrue(ids.remove("foo:choice")); assertTrue(ids.remove("px:bar-2.params-option-2")); assertTrue(ids.remove("px:script-option-1")); - assertTrue(ids.remove("transform-query")); // because o.d.p.modules.braille:common-utils on class path - assertTrue(ids.isEmpty()); + // assertTrue(ids.remove("transform-query")); // because braille-common on class path + // assertTrue(ids.remove("stylesheet-parameters")); // because css-utils on class path + // assertTrue(ids.remove("preview-table")); // because pef-utils on class path + //assertTrue(ids.remove("liblouis-table-query")); // because liblouis-utils on class path + assertTrue("ids not empty: " + ids, ids.isEmpty()); } @Inject @@ -60,15 +57,7 @@ protected String[] testDependencies() { return new String[]{ "org.daisy.pipeline:framework-core:?", "org.daisy.pipeline:calabash-adapter:?", - "org.daisy.pipeline.modules.braille:liblouis-utils:?" + // "org.daisy.pipeline.modules.braille:liblouis-utils:?" }; } - - @Override @Configuration - public Option[] config() { - return options( - // FIXME: BrailleUtils (dependency of liblouis-utils) needs older version of jing - mavenBundle("org.daisy.libs:jing:20120724.0.0"), - composite(super.config())); - } } diff --git a/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/CalabashWithPipelineModules.java b/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/CalabashWithPipelineModules.java index 2c028bea4f..76aa86b5b4 100644 --- a/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/CalabashWithPipelineModules.java +++ b/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/CalabashWithPipelineModules.java @@ -1,11 +1,6 @@ package org.daisy.pipeline.maven.plugin; -import java.io.File; -import java.net.MalformedURLException; import java.net.URI; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.Collection; import javax.xml.transform.Source; import javax.xml.transform.TransformerException; @@ -16,7 +11,7 @@ import org.daisy.maven.xproc.calabash.Calabash; -import org.daisy.pipeline.modules.impl.resolver.ModuleUriResolver; +import org.daisy.pipeline.modules.impl.ModuleUriResolver; import static org.daisy.pipeline.maven.plugin.utils.URLs.asURI; @@ -24,7 +19,7 @@ class CalabashWithPipelineModules extends Calabash { - CalabashWithPipelineModules(Collection classPath) { + CalabashWithPipelineModules(ClassLoader classPath) { super(); final URIResolver resolver = getModuleUriResolver(classPath); setURIResolver(new URIResolver() { @@ -43,24 +38,15 @@ public Source resolve(String href, String base) throws TransformerException { }); } - static URIResolver getModuleUriResolver(Collection classPath) { + // also used in HtmlizeSourcesMojo + static URIResolver getModuleUriResolver(ClassLoader classLoader) { ClassLoader restoreClassLoader = Thread.currentThread().getContextClassLoader(); try { - URLClassLoader classLoader; { - URL[] classPathURLs = new URL[classPath.size()]; { - int i = 0; - for (String path : classPath) - classPathURLs[i++] = new File(path).toURI().toURL(); - } - classLoader = new URLClassLoader(classPathURLs, Thread.currentThread().getContextClassLoader()); - } Thread.currentThread().setContextClassLoader(classLoader); for (URIResolver r : ServiceLoader.load(URIResolver.class)) if (r instanceof ModuleUriResolver) return r; throw new RuntimeException("No ModuleUriResolver found"); - } catch (MalformedURLException e) { - throw new RuntimeException("No ModuleUriResolver found", e); } finally { Thread.currentThread().setContextClassLoader(restoreClassLoader); } diff --git a/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/GenerateModuleClassFunctionProvider.java b/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/GenerateModuleClassFunctionProvider.java new file mode 100644 index 0000000000..63410c82cd --- /dev/null +++ b/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/GenerateModuleClassFunctionProvider.java @@ -0,0 +1,696 @@ +package org.daisy.pipeline.maven.plugin; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.nio.file.NoSuchFileException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.xml.namespace.QName; +import javax.xml.stream.XMLInputFactory; + +import org.apache.maven.plugin.logging.Log; + +import org.daisy.common.spi.ServiceLoader; +import org.daisy.common.xpath.saxon.ReflexiveExtensionFunctionProvider; +import org.daisy.pipeline.modules.Component; +import org.daisy.pipeline.modules.Dependency; +import org.daisy.pipeline.modules.JavaDependency; +import org.daisy.pipeline.modules.Module; +import org.daisy.pipeline.modules.ModuleRegistry; +import org.daisy.pipeline.modules.RelaxNGResource; +import org.daisy.pipeline.modules.ResourceLoader; +import org.daisy.pipeline.modules.UseXSLTPackage; +import org.daisy.pipeline.modules.XProcResource; +import org.daisy.pipeline.modules.XSLTPackage; +import org.daisy.pipeline.modules.XSLTResource; + +import org.osgi.framework.Version; + +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +public class GenerateModuleClassFunctionProvider extends ReflexiveExtensionFunctionProvider { + + private final XMLInputFactory xmlParser; + private final Set sourceRoots; + private final ModuleRegistry moduleRegistry; + private final Log logger; + + public GenerateModuleClassFunctionProvider(ClassLoader classPath, Collection sourceRoots, Log logger) { + super(GenerateModuleClass.class); + xmlParser = XMLInputFactory.newInstance(); + this.sourceRoots = new HashSet<>(); { + for (String dir : sourceRoots) { + File f = new File(dir); + if (f.exists()) + this.sourceRoots.add(f); }} + moduleRegistry = getModuleRegistry(classPath); + this.logger = logger; + } + + public class GenerateModuleClass { + + public String apply(String className, + String moduleName, + String moduleVersion, + String moduleTitle, + Element catalog) { + Set catalogEntries = parseCatalog(catalog); + URI catalogBaseURI = URI.create(catalog.getBaseURI()); + // fake module to pass to {XProc,XSLT,RelaxNG}Resource + Module moduleUnderCompilation = new Module(moduleName, moduleVersion, moduleTitle, new ResourceLoader() { + @Override + public URL loadResource(String path) throws NoSuchFileException { + URI uri = catalogBaseURI.resolve(path); + try { + // catalogBaseURI expected to be a file URI + if (!new File(uri).exists()) + throw new NoSuchFileException("file does not exist: " + path); + } catch (IllegalArgumentException e) { + throw new NoSuchFileException("file does not exist: " + path); + } + try { + return uri.toURL(); + } catch (MalformedURLException e) { + throw new IllegalStateException("coding error"); + } + } + @Override + public Iterable loadResources(String path) { + throw new UnsupportedOperationException(); + } + }) { + @Override + public void resolveDependencies() { + // no need to parse catalog.xml for this fake module + } + }; + // whether catalog.xml has entries with name + boolean hasCatalog = false; + // catalog entries with content-type="xslt-package" + Map xsltPackages = new HashMap<>(); + // dependency checks to be performed for a catalog entry when it has a name + Map componentDependencyChecks = new HashMap<>(); + // dependency checks to be performed for a catalog entry when it has content-type="xslt-package" + Map xsltPackageDependencyChecks = new HashMap<>(); + // dependency checks to be performed for a catalog entry when it is an internal resource + Map resourceDependencyChecks = new HashMap<>(); + // whether some components (of type XSLT, XProc or RelaxNG) depend on an external component (xsl:import, xsl:include, p:import, cx:import, grammar:include) + boolean componentChecks = false; + // whether some components (of type XSLT) depend on a (internal) Java class + boolean extensionFunctionChecks = false; + // whether some components (of type XSLT) depend on an XSLT package (xsl:use-package) + boolean xsltPackageChecks = false; + Set imports = new HashSet<>(); + for (CatalogEntry entry : catalogEntries) { + if (entry.name != null) + hasCatalog = true; + if (entry.contentType == ContentType.XSLT_PACKAGE) { + try { + XSLTPackage p = new XSLTPackage(moduleUnderCompilation, entry.uri.toString(), xmlParser); + xsltPackages.put(entry.uri, p); + } catch (NoSuchFileException e) { + throw new RuntimeException("Could not process catalog entry: resource not found: " + entry.uri.toString(), e); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Could not process catalog entry: invalid XSLT package: " + entry.uri.toString(), e); + } + } + Set dependencies; { + try { + if (entry.uri.toString().endsWith(".xpl")) + dependencies = new XProcResource(moduleUnderCompilation, entry.uri.toString()) + .listDependencies(moduleRegistry, sourceRoots, xmlParser); + else if (entry.uri.toString().endsWith(".xsl")) + dependencies = new XSLTResource(moduleUnderCompilation, entry.uri.toString()) + .listDependencies(moduleRegistry, sourceRoots, xmlParser); + else if (entry.uri.toString().endsWith(".rng")) + dependencies = new RelaxNGResource(moduleUnderCompilation, entry.uri.toString()) + .listDependencies(moduleRegistry, xmlParser); + else + dependencies = null; + } catch (NoSuchFileException e) { + throw new RuntimeException("Could not process catalog entry: resource not found: " + entry.uri.toString(), e); + } + } + if (dependencies != null && !dependencies.isEmpty()) { + StringBuilder s = new StringBuilder(); + for (Dependency dependency : dependencies) { + if (s.length() > 0) + s.append("\n"); + if (dependency instanceof Component) { + componentChecks = true; + Component component = (Component)dependency; + Version version = Version.parseVersion(component.getVersion()); + String versionRange = "[" + version + "," + new Version(version.getMajor() + 1, 0, 0) + ")"; + s.append("tryResolveComponent(\"" + component.getURI() + "\",\n"); + s.append(" \"" + versionRange + "\");"); + } else if (dependency instanceof UseXSLTPackage) { + xsltPackageChecks = true; + UseXSLTPackage usePackage = (UseXSLTPackage)dependency; + s.append("tryResolveXSLTPackage(\"" + usePackage.getName() + "\",\n"); + s.append(" \"" + usePackage.getVersion() + "\");"); + } else if (dependency instanceof JavaDependency) { + extensionFunctionChecks = true; + s.append("tryResolveXPathExtensionFunction(\"" + ((JavaDependency)dependency).getClassName() + "\");"); + } else + throw new IllegalStateException("Unknown dependency: " + dependency); + } + if (entry.name != null) + componentDependencyChecks.put(entry.name, s.toString()); + else if (entry.contentType == ContentType.XSLT_PACKAGE) + xsltPackageDependencyChecks.put(entry.uri.toString(), s.toString()); + else + resourceDependencyChecks.put(entry.uri.toString(), s.toString()); + } + } + if (!hasCatalog) + hasCatalog = catalog.getElementsByTagNameNS(CAT_PUBLIC.getNamespaceURI(), CAT_PUBLIC.getLocalPart()).getLength() > 0 || + catalog.getElementsByTagNameNS(CAT_SYSTEM.getNamespaceURI(), CAT_SYSTEM.getLocalPart()).getLength() > 0 || + catalog.getElementsByTagNameNS(CAT_REWRITE_URI.getNamespaceURI(), CAT_REWRITE_URI.getLocalPart()).getLength() > 0; + StringBuilder result = new StringBuilder(); + imports.add("org.daisy.pipeline.modules.Module"); + imports.add("org.daisy.pipeline.xmlcatalog.XmlCatalogParser"); + imports.add("org.osgi.service.component.annotations.Reference"); + imports.add("org.osgi.service.component.annotations.ReferenceCardinality"); + imports.add("org.osgi.service.component.annotations.ReferencePolicy"); + imports.add("org.slf4j.Logger"); + imports.add("org.slf4j.LoggerFactory"); + result.append("@"); + if (!componentDependencyChecks.isEmpty()) + result.append("org.osgi.service.component.annotations."); + else + imports.add("org.osgi.service.component.annotations.Component"); + result.append("Component(\n"); + result.append(" name = \"org.daisy.pipeline.modules.impl." + className + "\",\n"); + result.append(" service = { Module.class }\n"); + result.append(")\n"); + result.append("public class " + className + " extends Module {\n"); + result.append("\n"); + result.append(" private static final Logger logger = LoggerFactory.getLogger(" + className + ".class);\n"); + result.append("\n"); + if (hasCatalog) + result.append(" private boolean initialized = false;\n"); + result.append(" private XmlCatalogParser catalogParser;\n"); + if (!xsltPackageDependencyChecks.isEmpty()) { + imports.add("javax.xml.stream.XMLInputFactory"); + result.append(" private XMLInputFactory xmlParser;\n"); + } + if (extensionFunctionChecks) { + imports.add("org.daisy.common.xpath.saxon.XPathFunctionRegistry"); + result.append(" private XPathFunctionRegistry xpathFunctions;\n"); + } + if (xsltPackageChecks) { + imports.add("java.util.List"); + imports.add("net.sf.saxon.trans.packages.PackageDetails"); + result.append(" private List packageDetails;\n"); + } + if (componentChecks) { + imports.add("org.daisy.pipeline.modules.ModuleRegistry"); + result.append(" private ModuleRegistry moduleRegistry;\n"); + } + if (!componentDependencyChecks.isEmpty()) { + imports.add("java.util.LinkedList"); + imports.add("org.daisy.pipeline.modules.Component"); + result.append(" private final LinkedList componentsToAdd;\n"); + result.append(" private final LinkedList componentsBeingAdded;\n"); + } + if (!xsltPackageDependencyChecks.isEmpty()) { + imports.add("java.util.LinkedList"); + result.append(" private final LinkedList xsltPackagesToAdd;\n"); + result.append(" private final LinkedList xsltPackagesBeingAdded;\n"); + } + result.append("\n"); + result.append(" public " + className + "() {\n"); + result.append(" super(\"" + moduleName + "\",\n"); + result.append(" \"" + moduleVersion + "\",\n"); + result.append(" \"" + moduleTitle.replace("\"", "\\\"") + "\");\n"); + if (!componentDependencyChecks.isEmpty()) { + imports.add("java.util.LinkedList"); + result.append(" componentsToAdd = new LinkedList<>();\n"); + result.append(" componentsBeingAdded = new LinkedList<>();\n"); + } + if (!xsltPackageDependencyChecks.isEmpty()) { + imports.add("java.util.LinkedList"); + result.append(" xsltPackagesToAdd = new LinkedList<>();\n"); + result.append(" xsltPackagesBeingAdded = new LinkedList<>();\n"); + } + result.append(" }\n"); + result.append("\n"); + result.append(" @Reference(\n"); + result.append(" name = \"XmlCatalogParser\",\n"); + result.append(" unbind = \"-\",\n"); + result.append(" service = XmlCatalogParser.class,\n"); + result.append(" cardinality = ReferenceCardinality.MANDATORY,\n"); + result.append(" policy = ReferencePolicy.STATIC\n"); + result.append(" )\n"); + result.append(" public void setParser(XmlCatalogParser parser) {\n"); + result.append(" catalogParser = parser;\n"); + result.append(" }\n"); + result.append("\n"); + if (!xsltPackageDependencyChecks.isEmpty()) { + imports.add("javax.xml.stream.XMLInputFactory"); + result.append(" @Reference(\n"); + result.append(" name = \"XMLInputFactory\",\n"); + result.append(" unbind = \"-\",\n"); + result.append(" service = XMLInputFactory.class,\n"); + result.append(" cardinality = ReferenceCardinality.MANDATORY,\n"); + result.append(" policy = ReferencePolicy.STATIC\n"); + result.append(" )\n"); + result.append(" protected void setXMLInputFactory(XMLInputFactory parser) {\n"); + result.append(" xmlParser = parser;\n"); + result.append(" }\n"); + result.append("\n"); + } + if (componentChecks) { + imports.add("java.net.URI"); + imports.add("org.daisy.pipeline.modules.ModuleRegistry"); + imports.add("org.daisy.pipeline.modules.ResolutionException"); + result.append(" @Reference(\n"); + result.append(" name = \"ModuleRegistry\",\n"); + result.append(" unbind = \"-\",\n"); + result.append(" service = ModuleRegistry.class,\n"); + result.append(" cardinality = ReferenceCardinality.MANDATORY,\n"); + result.append(" policy = ReferencePolicy.STATIC\n"); + result.append(" )\n"); + result.append(" public void setModuleRegistry(ModuleRegistry registry) {\n"); + result.append(" moduleRegistry = registry;\n"); + result.append(" }\n"); + result.append("\n"); + result.append(" private void tryResolveComponent(String uri, String versionRange) throws ResolutionException {\n"); + result.append(" if (moduleRegistry.getModuleByComponent(URI.create(uri), versionRange) == null)\n"); + result.append(" throw new ResolutionException(\"Unresolved dependency \" + uri + \" with version=\" + versionRange);\n"); + result.append(" }\n"); + result.append("\n"); + } + if (!componentDependencyChecks.isEmpty() || !xsltPackageDependencyChecks.isEmpty()) { + imports.add("org.osgi.service.component.annotations.Activate"); + result.append(" @Activate\n"); + result.append(" protected void init() {\n"); + result.append(" logger.debug(\"Initializing module \" + getName());\n"); + if (!componentDependencyChecks.isEmpty()) { + imports.add("java.net.URI"); + imports.add("org.slf4j.helpers.NOPLogger"); + result.append(" Module tmpModule = new Module(getName(), getVersion(), getTitle()) {\n"); + result.append(" @Override public void resolveDependencies() {}\n"); + result.append(" @Override public Logger getLogger() { return NOPLogger.NOP_LOGGER; }\n"); + result.append(" };\n"); + result.append(" Module.parseCatalog(tmpModule, catalogParser);\n"); + result.append(" for (String e : tmpModule.getEntities()) {\n"); + result.append(" addEntity(tmpModule.getEntity(e));\n"); + result.append(" logger.debug(\"Loaded entity: \" + e);\n"); + result.append(" }\n"); + result.append(" for (URI c : tmpModule.getComponents()) {\n"); + result.append(" components.put(c, () -> null); // will be replaced during resolveDependencies()\n"); + result.append(" componentsToAdd.add(tmpModule.getComponent(c));\n"); + result.append(" }\n"); + } else + result.append(" Module.parseCatalog(this, catalogParser);\n"); + for (URI path : xsltPackages.keySet()) { + XSLTPackage p = xsltPackages.get(path); + if (xsltPackageDependencyChecks.containsKey(path.toString())) { + result.append(" xsltPackagesToAdd.add(\"" + path + "\");\n"); + } else { + imports.add("java.nio.file.NoSuchFileException"); + result.append(" try {\n"); + result.append(" addXSLTPackage(\"" + p.getName() + "\",\n"); + result.append(" \"" + p.getVersion() + "\",\n"); + result.append(" \"" + path + "\");\n"); + result.append(" } catch (NoSuchFileException e) {\n"); + result.append(" logger.warn(\"XSLT package " + p.getName() + " can not be loaded: \" + e.getMessage());\n"); + result.append(" }\n"); + } + } + result.append(" logger.debug(\"Module \" + getName() + \" initialized, but dependencies have not been resolved yet\");\n"); + result.append(" }\n"); + result.append("\n"); + result.append(" // Not calling resolveDependencies() during object construction yet because it depends\n"); + result.append(" // on other modules, and other modules may depend on this module. resolveDependencies()\n"); + result.append(" // will be called when the object gets bound in ModuleRegistry. The method may recursively\n"); + result.append(" // call itself. It copes with that by activating components in a different order\n"); + result.append(" // when called recursively. ModuleRegistry has protection against endless recursion.\n"); + result.append(" @Override\n"); + result.append(" public void resolveDependencies() {\n"); + result.append(" if (!initialized) {\n"); + if (!componentDependencyChecks.isEmpty() && !xsltPackageDependencyChecks.isEmpty()) + result.append(" boolean recursive = !componentsBeingAdded.isEmpty() || !xsltPackagesBeingAdded.isEmpty();\n"); + else if (!componentDependencyChecks.isEmpty()) + result.append(" boolean recursive = !componentsBeingAdded.isEmpty();\n"); + else + result.append(" boolean recursive = !xsltPackagesBeingAdded.isEmpty();\n"); + result.append(" if (recursive)\n"); + result.append(" logger.debug(\"Resolving dependencies of module \" + getName() + \" (continuing in recursive call)\");\n"); + result.append(" else\n"); + result.append(" logger.debug(\"Resolving dependencies of module \" + getName());\n"); + if (!componentDependencyChecks.isEmpty()) { + imports.add("org.daisy.pipeline.modules.Component"); + result.append(" while (true) {\n"); + result.append(" if (componentsToAdd.isEmpty()) {\n"); + result.append(" if (componentsBeingAdded.isEmpty())\n"); + result.append(" break;\n"); + result.append(" componentsToAdd.addAll(componentsBeingAdded);\n"); + result.append(" componentsBeingAdded.clear();\n"); + result.append(" }\n"); + result.append(" Component c = componentsToAdd.poll();\n"); + result.append(" componentsBeingAdded.add(c);\n"); + result.append(" if (addComponent(c)) // may call resolveDependencies() recursively\n"); + result.append(" logger.debug(\"Loaded component: \" + c.getURI());\n"); + result.append(" componentsBeingAdded.remove(c);\n"); + result.append(" componentsToAdd.remove(c);\n"); + result.append(" }\n"); + } + if (!xsltPackageDependencyChecks.isEmpty()) { + imports.add("java.nio.file.NoSuchFileException"); + result.append(" while (true) {\n"); + result.append(" if (xsltPackagesToAdd.isEmpty()) {\n"); + result.append(" if (xsltPackagesBeingAdded.isEmpty())\n"); + result.append(" break;\n"); + result.append(" xsltPackagesToAdd.addAll(xsltPackagesBeingAdded);\n"); + result.append(" xsltPackagesBeingAdded.clear();\n"); + result.append(" }\n"); + result.append(" String p = xsltPackagesToAdd.poll();\n"); + result.append(" logger.debug(\"Loading XSLT package in module \" + getName() + \": \" + p);\n"); + result.append(" xsltPackagesBeingAdded.add(p);\n"); + result.append(" try {\n"); + result.append(" addXSLTPackage(p, xmlParser); // may call resolveDependencies() recursively\n"); + result.append(" } catch (NoSuchFileException e) {\n"); + result.append(" logger.warn(\"XSLT package can not be loaded: \" + e.getMessage());\n"); + result.append(" } catch (IllegalArgumentException e) {\n"); + result.append(" throw new IllegalStateException(); // should not happen: file validated during compilation\n"); + result.append(" }\n"); + result.append(" xsltPackagesBeingAdded.remove(p);\n"); + result.append(" xsltPackagesToAdd.remove(p);\n"); + result.append(" }\n"); + } + result.append(" if (!initialized)\n"); + result.append(" logger.debug(\"Done resolving dependencies of module \" + getName());\n"); + result.append(" initialized = true;\n"); + result.append(" }\n"); + result.append(" }\n"); + if (!componentDependencyChecks.isEmpty()) { + imports.add("org.daisy.pipeline.modules.Component"); + imports.add("org.daisy.pipeline.modules.ResolutionException"); + result.append("\n"); + result.append(" @Override\n"); + result.append(" protected boolean addComponent(Component component) {\n"); + result.append(" String name = component.getURI().toString();\n"); + result.append(" try {\n"); + boolean first = true; + for (URI componentName : componentDependencyChecks.keySet()) { + if (first) + result.append(" "); + else + result.append(" else "); + first = false; + result.append("if (\"" + componentName + "\".equals(name)) {\n"); + for (String s : componentDependencyChecks.get(componentName).split("\n")) + result.append(" ").append(s).append("\n"); + result.append(" }"); + } + result.append("\n"); + result.append(" } catch (ResolutionException re) {\n"); + result.append(" logger.warn(\"Component \" + component.getURI() + \" can not be loaded: \" + re.getMessage());\n"); + result.append(" return false;\n"); + result.append(" }\n"); + result.append(" return super.addComponent(component);\n"); + result.append(" }\n"); + } + } else { + if (hasCatalog || !xsltPackages.isEmpty()) { + imports.add("org.osgi.service.component.annotations.Activate"); + result.append(" @Activate\n"); + result.append(" protected void init() {\n"); + result.append(" logger.debug(\"Initializing module \" + getName());\n"); + if (hasCatalog) + result.append(" Module.parseCatalog(this, catalogParser);\n"); + for (URI path : xsltPackages.keySet()) { + imports.add("java.nio.file.NoSuchFileException"); + XSLTPackage p = xsltPackages.get(path); + result.append(" try {\n"); + result.append(" addXSLTPackage(\"" + p.getName() + "\",\n"); + result.append(" \"" + p.getVersion() + "\",\n"); + result.append(" \"" + path + "\");\n"); + result.append(" } catch (NoSuchFileException e) {\n"); + result.append(" logger.warn(\"XSLT package " + p.getName() + " can not be loaded: \" + e.getMessage());\n"); + result.append(" }\n"); + } + result.append(" logger.debug(\"Module \" + getName() + \" initialized\");\n"); + result.append(" }\n"); + result.append("\n"); + } + result.append(" @Override\n"); + result.append(" public void resolveDependencies() {\n"); + result.append(" logger.debug(\"No dependencies to resolve for module \" + getName());\n"); + result.append(" }\n"); + } + if (extensionFunctionChecks) { + imports.add("java.nio.file.NoSuchFileException"); + imports.add("net.sf.saxon.lib.ExtensionFunctionDefinition"); + imports.add("org.daisy.common.xpath.saxon.XPathFunctionRegistry"); + imports.add("org.daisy.pipeline.modules.ResolutionException"); + result.append("\n"); + result.append(" @Reference(\n"); + result.append(" name = \"XPathFunctionRegistry\",\n"); + result.append(" unbind = \"-\",\n"); + result.append(" service = XPathFunctionRegistry.class,\n"); + result.append(" cardinality = ReferenceCardinality.MANDATORY,\n"); + result.append(" policy = ReferencePolicy.STATIC\n"); + result.append(" )\n"); + result.append(" public void setXPathFunctionRegistry(XPathFunctionRegistry registry) {\n"); + result.append(" xpathFunctions = registry;\n"); + result.append(" }\n"); + result.append("\n"); + result.append(" private void tryResolveXPathExtensionFunction(String className) throws ResolutionException {\n"); + result.append(" for (ExtensionFunctionDefinition d : xpathFunctions.getFunctions()) {\n"); + result.append(" if (className.equals(d.getFunctionQName().getURI())) {\n"); + result.append(" try {\n"); + result.append(" getResource(\"../\" + className.replace('.', '/') + \".class\");\n"); + result.append(" return;\n"); + result.append(" } catch (NoSuchFileException e) {\n"); + result.append(" logger.debug(\n"); + result.append(" \"Class \" + className + \" expected to be in the same module (\" + getName() + \").\");\n"); + result.append(" }\n"); + result.append(" }\n"); + result.append(" }\n"); + result.append(" throw new ResolutionException(\n"); + result.append(" \"Unresolved Java dependency: no class \" + className + \" found in module \" + getName());\n"); + result.append(" }\n"); + } + if (xsltPackageChecks) { + imports.add("java.util.ArrayList"); + imports.add("net.sf.saxon.style.PackageVersion"); + imports.add("net.sf.saxon.style.PackageVersionRanges"); + imports.add("net.sf.saxon.trans.packages.PackageDetails"); + imports.add("net.sf.saxon.trans.XPathException"); + imports.add("org.daisy.pipeline.modules.ResolutionException"); + imports.add("org.daisy.pipeline.modules.XSLTPackage"); + result.append("\n"); + result.append(" @Reference(\n"); + result.append(" name = \"PackageDetails\",\n"); + result.append(" unbind = \"-\",\n"); + result.append(" service = PackageDetails.class,\n"); + result.append(" cardinality = ReferenceCardinality.MULTIPLE,\n"); + result.append(" policy = ReferencePolicy.STATIC\n"); + result.append(" )\n"); + result.append(" public void addPackageDetails(PackageDetails pack) {\n"); + result.append(" if (packageDetails == null)\n"); + result.append(" packageDetails = new ArrayList<>();\n"); + result.append(" packageDetails.add(pack);\n"); + result.append(" }\n"); + result.append("\n"); + result.append(" private void tryResolveXSLTPackage(String name, String versionRange) throws ResolutionException{\n"); + result.append(" if (packageDetails != null) {\n"); + result.append(" for (PackageDetails pack : packageDetails) {\n"); + result.append(" if (name.equals(pack.nameAndVersion.packageName)) {\n"); + result.append(" if (versionRange != null) {\n"); + result.append(" // see https://www.w3.org/TR/xslt-30/#package-versions\n"); + result.append(" try {\n"); + result.append(" if (new PackageVersionRanges(versionRange).contains(pack.nameAndVersion.packageVersion));\n"); + result.append(" return true;\n"); + result.append(" else\n"); + result.append(" logger.debug(\"Version of \" + name + \" (\" + pack.nameAndVersion.packageVersion + \") \n"); + result.append(" + \"not in requested range (\" + versionRange + \")\");\n"); + result.append(" } catch (XPathException e) {\n"); + result.append(" \n"); + result.append(" }\n"); + result.append(" } else {\n"); + result.append(" return;\n"); + result.append(" }\n"); + result.append(" }\n"); + result.append(" }\n"); + result.append(" }\n"); + result.append(" if (getModuleByXSLTPackage(name, versionRange) != null)\n"); + result.append(" return;\n"); + result.append(" throw new ResolutionException(\"Unresolved XSLT package \" + name + \" with version=\" + versionRange);\n"); + result.append(" }\n"); + result.append("\n"); + result.append(" public Module getModuleByXSLTPackage(String name, String versionRange) {\n"); + result.append(" Module module = moduleRegistry.getModuleByXSLTPackage(name);\n"); + result.append(" if (module != null && versionRange != null) {\n"); + result.append(" XSLTPackage pack = module.getXSLTPackage(name);\n"); + result.append(" if (pack == null)\n"); + result.append(" throw new IllegalStateException(\"coding error\"); // can not happen\n"); + result.append(" PackageVersion version; {\n"); + result.append(" try {\n"); + result.append(" version = new PackageVersion(pack.getVersion());\n"); + result.append(" } catch (XPathException e) {\n"); + result.append(" logger.debug(\"Version of \" + name + \" can not be parsed: \" + pack.getVersion());\n"); + result.append(" return null;\n"); + result.append(" }\n"); + result.append(" }\n"); + result.append(" try {\n"); + result.append(" if (!(new PackageVersionRanges(versionRange).contains(version))) {\n"); + result.append(" logger.debug(\"Version of \" + name + \" (\" + version + \") not in requested range (\" + versionRange + \")\");\n"); + result.append(" return null;\n"); + result.append(" }\n"); + result.append(" } catch (XPathException e) {\n"); + result.append(" logger.debug(\"Version range can not be parsed: \" + versionRange);\n"); + result.append(" return null;\n"); + result.append(" }\n"); + result.append(" }\n"); + result.append(" return module;\n"); + result.append(" }\n"); + } + if (!resourceDependencyChecks.isEmpty() || !xsltPackageDependencyChecks.isEmpty()) { + imports.add("java.net.URL"); + imports.add("java.nio.file.NoSuchFileException"); + imports.add("org.daisy.pipeline.modules.ResolutionException"); + resourceDependencyChecks.putAll(xsltPackageDependencyChecks); + result.append("\n"); + for (int i = 1; i <= resourceDependencyChecks.size(); i++) { + result.append(" private URL resource" + i + " = null;\n"); + } + result.append("\n"); + result.append(" @Override\n"); + result.append(" public URL getResource(String path) throws NoSuchFileException {\n"); + result.append(" URL url = super.getResource(path);\n"); + result.append(" try {\n"); + int i = 1; + for (String path : resourceDependencyChecks.keySet()) { + result.append(" if (resource" + i + " == null)\n"); + result.append(" resource" + i + " = super.getResource(\"" + path + "\");\n"); + result.append(" if (url.equals(resource" + i + ")) {\n"); + for (String s : resourceDependencyChecks.get(path).split("\n")) { + result.append(" ").append(s).append("\n"); + } + result.append(" return url;\n"); + result.append(" }\n"); + i++; + } + result.append(" } catch (ResolutionException re) {\n"); + result.append(" throw new NoSuchFileException(\n"); + result.append(" \"Resource \" + path + \" in module \" + getName() + \" can not be loaded: \""); + result.append(" + re.getMessage());\n"); + result.append(" }\n"); + result.append(" return url;\n"); + result.append(" }\n"); + } + result.append("}\n"); + // insert package and imports + if (imports.size() > 0) { + List sortedImports = new ArrayList<>(imports); + Collections.sort(sortedImports); + String lastPrefix = null; + StringBuilder s = new StringBuilder(); + for (String i : sortedImports) { + String prefix = i.replaceAll("^([^.]+)\\..*$", "$1"); + if (lastPrefix == null) + lastPrefix = prefix; + else if (!lastPrefix.equals(prefix)) { + lastPrefix = prefix; + s.append("\n"); + } + s.append("import ").append(i).append(";\n"); + } + result.insert(0, "\n").insert(0, s); + } + result.insert(0, "package org.daisy.pipeline.modules.impl;\n\n"); + logger.debug("Successfully generated class " + className); + return result.toString(); + } + } + + private static final String XML_CATALOG_NS = "urn:oasis:names:tc:entity:xmlns:xml:catalog"; + private static final QName CAT_URI = new QName(XML_CATALOG_NS, "uri"); + private static final QName CAT_PUBLIC = new QName(XML_CATALOG_NS, "public"); + private static final QName CAT_SYSTEM = new QName(XML_CATALOG_NS, "system"); + private static final QName CAT_REWRITE_URI = new QName(XML_CATALOG_NS, "rewriteURI"); + private static final QName _NAME = new QName("name"); + private static final QName _URI = new QName("uri"); + private static final String PIPELINE_EXT_NS = "http://www.daisy.org/ns/pipeline"; + private static final QName PX_CONTENT_TYPE = new QName(PIPELINE_EXT_NS, "content-type"); + + private static Set parseCatalog(Element catalog) { + Set entries = new HashSet<>(); + NodeList children = catalog.getElementsByTagNameNS(CAT_URI.getNamespaceURI(), CAT_URI.getLocalPart()); + for (int i = 0; i < children.getLength(); i++) { + Element e = (Element)children.item(i); + Attr name = e.getAttributeNodeNS(_NAME.getNamespaceURI(), _NAME.getLocalPart()); + Attr uri = e.getAttributeNodeNS(_URI.getNamespaceURI(), _URI.getLocalPart()); + Attr contentType = e.getAttributeNodeNS(PX_CONTENT_TYPE.getNamespaceURI(), PX_CONTENT_TYPE.getLocalPart()); + entries.add(new CatalogEntry(name != null ? URI.create(name.getValue()) : null, + uri != null ? URI.create(uri.getValue()) : null, + contentType != null ? ContentType.create(contentType.getValue()) : null)); + } + return entries; + } + + private static class CatalogEntry { + public final URI name; + public final URI uri; + public final ContentType contentType; + public CatalogEntry(URI name, URI uri, ContentType contentType) { + if (uri == null) + throw new IllegalArgumentException("uri must not be null"); + this.name = name; + this.uri = uri; + this.contentType = contentType; + } + } + + private static enum ContentType { + + SCRIPT("script"), + DATA_TYPE("data-type"), + XSLT_PACKAGE("xslt-package"), + PARAMS("params"), + CALABASH_CONFIG("calabash-config"), + LIBLOUIS_TABLES("liblouis-tables"), + LIBHYPHEN_TABLES("libhyphen-tables"), + USER_AGENT_STYLESHEET("user-agent-stylesheet"); + + private final String type; + + private ContentType(String type) { + this.type = type; + } + + public static ContentType create(String type) { + for (ContentType t : ContentType.values()) + if (t.type.equals(type)) + return t; + throw new IllegalArgumentException("Not a valid content-type: " + type); + } + } + + private static ModuleRegistry getModuleRegistry(ClassLoader classLoader) { + ClassLoader restoreClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(classLoader); + for (ModuleRegistry r : ServiceLoader.load(ModuleRegistry.class)) + return r; + throw new RuntimeException("No ModuleRegistry found"); + } finally { + Thread.currentThread().setContextClassLoader(restoreClassLoader); + } + } +} diff --git a/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/HtmlizeSourcesMojo.java b/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/HtmlizeSourcesMojo.java index ee26d0353b..b125dfaf73 100644 --- a/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/HtmlizeSourcesMojo.java +++ b/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/HtmlizeSourcesMojo.java @@ -32,11 +32,12 @@ import org.daisy.maven.xproc.api.XProcEngine; import org.daisy.maven.xproc.api.XProcExecutionException; +import static org.daisy.pipeline.maven.plugin.utils.getClassLoader; import static org.daisy.pipeline.maven.plugin.utils.URLs.asURI; import static org.daisy.pipeline.maven.plugin.utils.URLs.relativize; import static org.daisy.pipeline.maven.plugin.utils.XML.evaluateXPath; import static org.daisy.pipeline.maven.plugin.utils.XML.transform; -import org.daisy.pipeline.modules.impl.resolver.ModuleUriResolver; +import org.daisy.pipeline.modules.impl.ModuleUriResolver; /** * @goal htmlize-sources @@ -78,7 +79,7 @@ public class HtmlizeSourcesMojo extends AbstractMojo { public void execute() throws MojoFailureException { try { - final List compileClassPath = mavenProject.getCompileClasspathElements(); + final ClassLoader compileClassPath = getClassLoader(mavenProject.getCompileClasspathElements()); final XProcEngine engine = new CalabashWithPipelineModules(compileClassPath); List sources = new ArrayList(); if (includes == null) diff --git a/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/ProcessCatalogMojo.java b/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/ProcessCatalogMojo.java index d0f31fc96c..ca7cedfddd 100644 --- a/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/ProcessCatalogMojo.java +++ b/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/ProcessCatalogMojo.java @@ -9,6 +9,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import net.sf.saxon.lib.ExtensionFunctionDefinition; + import org.apache.maven.model.Resource; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoFailureException; @@ -18,8 +20,9 @@ import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; -import org.daisy.maven.xproc.api.XProcEngine; +import org.daisy.common.xpath.saxon.ExtensionFunctionProvider; +import static org.daisy.pipeline.maven.plugin.utils.getClassLoader; import static org.daisy.pipeline.maven.plugin.utils.URLs.asURI; @Mojo( @@ -85,7 +88,18 @@ public void execute() throws MojoFailureException { getLog().info("Skipping the execution."); return; } try { - XProcEngine engine = new CalabashWithPipelineModules(mavenProject.getCompileClasspathElements()); + ClassLoader compileClassPath = getClassLoader(mavenProject.getCompileClasspathElements()); + CalabashWithPipelineModules engine = new CalabashWithPipelineModules(compileClassPath); + ExtensionFunctionProvider generateModuleClassFunctionProvider = new GenerateModuleClassFunctionProvider( + compileClassPath, + mavenProject.getCompileSourceRoots(), + getLog()); + engine.setConfiguration( + config -> { + for (ExtensionFunctionDefinition function : generateModuleClassFunctionProvider.getDefinitions()) + config.getProcessor().getUnderlyingConfiguration().registerExtensionFunction(function); + } + ); Map options = new HashMap(); { options.put("generatedResourcesDirectory", asURI(generatedResourcesDirectory).toASCIIString()); options.put("generatedSourcesDirectory", asURI(generatedSourcesDirectory).toASCIIString()); diff --git a/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/ProcessTestCatalogMojo.java b/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/ProcessTestCatalogMojo.java index fc36c1d72b..3c4fc66f67 100644 --- a/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/ProcessTestCatalogMojo.java +++ b/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/ProcessTestCatalogMojo.java @@ -9,6 +9,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import net.sf.saxon.lib.ExtensionFunctionDefinition; + import org.apache.maven.model.Resource; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoFailureException; @@ -18,8 +20,9 @@ import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; -import org.daisy.maven.xproc.api.XProcEngine; +import org.daisy.common.xpath.saxon.ExtensionFunctionProvider; +import static org.daisy.pipeline.maven.plugin.utils.getClassLoader; import static org.daisy.pipeline.maven.plugin.utils.URLs.asURI; @Mojo( @@ -85,7 +88,18 @@ public void execute() throws MojoFailureException { getLog().info("Tests are skipped."); return; } try { - XProcEngine engine = new CalabashWithPipelineModules(mavenProject.getCompileClasspathElements()); + ClassLoader compileClassPath = getClassLoader(mavenProject.getCompileClasspathElements()); + CalabashWithPipelineModules engine = new CalabashWithPipelineModules(compileClassPath); + ExtensionFunctionProvider generateModuleClassFunctionProvider = new GenerateModuleClassFunctionProvider( + compileClassPath, + mavenProject.getCompileSourceRoots(), + getLog()); + engine.setConfiguration( + config -> { + for (ExtensionFunctionDefinition function : generateModuleClassFunctionProvider.getDefinitions()) + config.getProcessor().getUnderlyingConfiguration().registerExtensionFunction(function); + } + ); Map options = new HashMap(); { options.put("generatedResourcesDirectory", asURI(generatedResourcesDirectory).toASCIIString()); options.put("generatedSourcesDirectory", asURI(generatedSourcesDirectory).toASCIIString()); diff --git a/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/utils.java b/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/utils.java index aba441219e..db78101176 100644 --- a/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/utils.java +++ b/utils/build-utils/modules-build-helper/src/main/java/org/daisy/pipeline/maven/plugin/utils.java @@ -5,8 +5,10 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.net.URLClassLoader; import java.net.URLDecoder; import java.util.ArrayList; +import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -31,6 +33,19 @@ abstract class utils { + static ClassLoader getClassLoader(Collection classPath) { + try { + URL[] classPathURLs = new URL[classPath.size()]; { + int i = 0; + for (String path : classPath) + classPathURLs[i++] = new File(path).toURI().toURL(); + } + return new URLClassLoader(classPathURLs, Thread.currentThread().getContextClassLoader()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + static abstract class URLs { static URI asURI(File file) { diff --git a/utils/build-utils/modules-build-helper/src/main/resources/process-catalog/process-catalog.xsl b/utils/build-utils/modules-build-helper/src/main/resources/process-catalog/process-catalog.xsl index 8262a5fbb2..bbc84251ea 100644 --- a/utils/build-utils/modules-build-helper/src/main/resources/process-catalog/process-catalog.xsl +++ b/utils/build-utils/modules-build-helper/src/main/resources/process-catalog/process-catalog.xsl @@ -10,6 +10,7 @@ xmlns:html="http://www.w3.org/1999/xhtml" xmlns:f="http://www.daisy.org/ns/pipeline/internal-functions" xmlns:pf="http://www.daisy.org/ns/pipeline/functions" + xmlns:GenerateModuleClass="org.daisy.pipeline.maven.plugin.GenerateModuleClassFunctionProvider$GenerateModuleClass" exclude-result-prefixes="#all" version="2.0"> @@ -58,16 +59,6 @@ - - - - Require-Bundle: - - - - @@ -89,56 +80,23 @@ - - + + + + + - package org.daisy.pipeline.modules.impl; - -import org.daisy.pipeline.modules.Module; -import org.daisy.pipeline.xmlcatalog.XmlCatalogParser; - -import org.osgi.service.component.annotations.Activate; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Reference; -import org.osgi.service.component.annotations.ReferenceCardinality; -import org.osgi.service.component.annotations.ReferencePolicy; - -@Component( - name = "org.daisy.pipeline.modules.impl.", - service = { Module.class } -) -public class extends Module { - - private XmlCatalogParser catalogParser; - - public () { - super("", - "", - ""); - } - - @Activate - public void activate() { - super.init(catalogParser); - } - - @Reference( - name = "XmlCatalogParser", - unbind = "-", - service = XmlCatalogParser.class, - cardinality = ReferenceCardinality.MANDATORY, - policy = ReferencePolicy.STATIC - ) - public void setParser(XmlCatalogParser parser) { - catalogParser = parser; - } -} - + @@ -147,12 +105,11 @@ public class extends Module { - + - + + + @@ -203,7 +160,7 @@ public class extends Module { 'liblouis-tables', 'libhyphen-tables')]| cat:uri/@px:content-type[.=('script', - 'xsl-package', + 'xslt-package', 'data-type', 'params', 'user-agent-stylesheet', @@ -288,65 +245,6 @@ public class extends XProcScriptService { } - - - - not a xsl:package: - - - missing @name: - - - - - - - - - - - - - - - - package org.daisy.common.saxon.impl; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.net.URL; -import javax.xml.transform.stream.StreamSource; - -import net.sf.saxon.trans.packages.PackageDetails; -import net.sf.saxon.trans.packages.VersionedPackageName; -import net.sf.saxon.trans.XPathException; - -import org.daisy.common.file.URLs; - -import org.osgi.service.component.annotations.Component; - -@Component( - name = "", - service = { PackageDetails.class } -) -public class extends PackageDetails { - public () { - try { - nameAndVersion = new VersionedPackageName("", ""); - URL url = URLs.getResourceFromJAR("", - .class); - sourceLocation = new StreamSource(url.openStream(), url.toURI().toASCIIString()); - } catch (XPathException|IOException|URISyntaxException e) { - throw new IllegalStateException( - "Failed to create PackageDetails object for package ''", e); - } - } -} - - surefire-report:report @@ -146,8 +147,4 @@ - - - v1.0.3 - diff --git a/utils/xproc-maven-plugin/xproc-maven-plugin/src/it/it-xprocspec-catalog-calabash/pom.xml b/utils/xproc-maven-plugin/xproc-maven-plugin/src/it/it-xprocspec-catalog-calabash/pom.xml index b44e94c196..7c2b3ace44 100644 --- a/utils/xproc-maven-plugin/xproc-maven-plugin/src/it/it-xprocspec-catalog-calabash/pom.xml +++ b/utils/xproc-maven-plugin/xproc-maven-plugin/src/it/it-xprocspec-catalog-calabash/pom.xml @@ -7,6 +7,15 @@ it-xprocspec-catalog-calabash 0.0-SNAPSHOT + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 2.14.1 + + + org.daisy.maven diff --git a/utils/xproc-maven-plugin/xproc-maven-plugin/src/it/it-xprocspec-config-calabash/pom.xml b/utils/xproc-maven-plugin/xproc-maven-plugin/src/it/it-xprocspec-config-calabash/pom.xml index a392e7ac4e..621972f6b0 100644 --- a/utils/xproc-maven-plugin/xproc-maven-plugin/src/it/it-xprocspec-config-calabash/pom.xml +++ b/utils/xproc-maven-plugin/xproc-maven-plugin/src/it/it-xprocspec-config-calabash/pom.xml @@ -58,6 +58,11 @@ 1.6 + + org.apache.maven.plugins + maven-surefire-report-plugin + 2.14.1 + diff --git a/utils/xproc-maven-plugin/xproc-maven-plugin/src/it/it-xprocspec-error-calabash/pom.xml b/utils/xproc-maven-plugin/xproc-maven-plugin/src/it/it-xprocspec-error-calabash/pom.xml index 28a00af63a..d9ad32fe9a 100644 --- a/utils/xproc-maven-plugin/xproc-maven-plugin/src/it/it-xprocspec-error-calabash/pom.xml +++ b/utils/xproc-maven-plugin/xproc-maven-plugin/src/it/it-xprocspec-error-calabash/pom.xml @@ -7,6 +7,15 @@ it-xprocspec-error-calabash 0.0-SNAPSHOT + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 2.14.1 + + + org.daisy.maven diff --git a/utils/xproc-maven-plugin/xproc-maven-plugin/src/it/it-xprocspec-hello-world-calabash/pom.xml b/utils/xproc-maven-plugin/xproc-maven-plugin/src/it/it-xprocspec-hello-world-calabash/pom.xml index aa72dc54fe..42548baec9 100644 --- a/utils/xproc-maven-plugin/xproc-maven-plugin/src/it/it-xprocspec-hello-world-calabash/pom.xml +++ b/utils/xproc-maven-plugin/xproc-maven-plugin/src/it/it-xprocspec-hello-world-calabash/pom.xml @@ -7,6 +7,15 @@ it-xprocspec-hello-world-calabash 0.0-SNAPSHOT + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 2.14.1 + + + org.daisy.maven @@ -30,13 +39,4 @@ - - - - org.apache.maven.plugins - maven-surefire-report-plugin - 2.14.1 - - - diff --git a/utils/xproc-maven-plugin/xprocspec-runner/pom.xml b/utils/xproc-maven-plugin/xprocspec-runner/pom.xml index 5a30c35ba7..9f81d80517 100644 --- a/utils/xproc-maven-plugin/xprocspec-runner/pom.xml +++ b/utils/xproc-maven-plugin/xprocspec-runner/pom.xml @@ -12,7 +12,7 @@ org.daisy.maven xprocspec-runner - 1.2.7 + 1.2.8-SNAPSHOT bundle xprocspec-runner @@ -21,7 +21,7 @@ org.daisy.pipeline framework-bom - 1.14.16 + 1.14.21-SNAPSHOT pom import @@ -117,7 +117,7 @@ org.daisy.pipeline.build modules-build-helper - 2.5.0 + 3.0.0-SNAPSHOT process-catalog From dbce9a4335e0ea3e22c875a44a41da70b249bb54 Mon Sep 17 00:00:00 2001 From: Bert Frees Date: Mon, 11 Nov 2024 20:22:22 +0100 Subject: [PATCH 04/15] git subrepo pull utils/build-utils subrepo: subdir: "utils/build-utils" merged: "b71b62133a" upstream: origin: "git@github.com:daisy/pipeline-build-utils.git" branch: "master" commit: "7111f4b73a" git-subrepo: version: "0.3.1" origin: "???" commit: "???" --- utils/build-utils/.gitrepo | 2 +- utils/build-utils/modules-test-helper/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/build-utils/.gitrepo b/utils/build-utils/.gitrepo index 42a04b108a..614cf625f2 100644 --- a/utils/build-utils/.gitrepo +++ b/utils/build-utils/.gitrepo @@ -7,5 +7,5 @@ remote = git@github.com:daisy/pipeline-build-utils.git branch = master commit = 7111f4b73a29a34798d175b7c0998c722e02e6f8 - parent = fc27d04c4788948cce4b9d43ac78d895376c24e2 + parent = d26821962b8c2907b30da1316d38b9848637bd03 cmdver = 0.3.1 diff --git a/utils/build-utils/modules-test-helper/pom.xml b/utils/build-utils/modules-test-helper/pom.xml index 56326e5af6..3914c765d2 100644 --- a/utils/build-utils/modules-test-helper/pom.xml +++ b/utils/build-utils/modules-test-helper/pom.xml @@ -40,7 +40,7 @@ org.daisy.maven xprocspec-runner - 1.2.7 + 1.2.8-SNAPSHOT org.daisy.maven From 5dc90656f8f1494c02a5a0f12ae763d23fce4039 Mon Sep 17 00:00:00 2001 From: Bert Frees Date: Tue, 12 Nov 2024 11:02:27 +0100 Subject: [PATCH 05/15] git subrepo pull website subrepo: subdir: "website" merged: "a8d1a3e3dd" upstream: origin: "git@github.com:daisy/pipeline.git" branch: "website" commit: "a8d1a3e3dd" git-subrepo: version: "0.3.1" origin: "???" commit: "???" --- website/.gitrepo | 4 +- website/Makefile | 25 +++--- website/src/Download.md | 2 + website/src/_includes/download-link | 2 +- website/src/_wiki/Get-Help/User-Guide.md | 33 ++++--- .../_wiki/Get-Help/User-Guide/Installation.md | 22 ++--- .../User-Guide/Installation/disk-image.png | Bin 240768 -> 0 bytes website/src/_wiki/Support.md | 11 +++ website/src/_wiki/_Sidebar.md | 4 +- website/src/index.md | 81 ++++++++++-------- .../src/js/post-download-donate-message.js | 12 +++ 11 files changed, 115 insertions(+), 81 deletions(-) delete mode 100644 website/src/_wiki/Get-Help/User-Guide/Installation/disk-image.png create mode 100644 website/src/_wiki/Support.md create mode 100644 website/src/js/post-download-donate-message.js diff --git a/website/.gitrepo b/website/.gitrepo index 8dfe8f4e48..c52fb83e2b 100644 --- a/website/.gitrepo +++ b/website/.gitrepo @@ -6,6 +6,6 @@ [subrepo] remote = git@github.com:daisy/pipeline.git branch = website - commit = 40bdfe8c0b30a012a6d6c92bb185343fd4775d35 - parent = c844c9642350493e191304e1cdffdfe116ae2a5b + commit = a8d1a3e3dd391f31e77eb2b2af994f49a077b14b + parent = b40a9bbc10ff80e6ff32e94df49a603fb9cb8c67 cmdver = 0.3.1 diff --git a/website/Makefile b/website/Makefile index 04fc45f300..c4b4821600 100644 --- a/website/Makefile +++ b/website/Makefile @@ -1,8 +1,16 @@ MVN ?= mvn JEKYLL_SRC_DIR := src SHELL := bash +# to use ruby 2.7 on macOS: +# - brew tap homebrew/core --force +# - brew edit ruby@2 +# - comment out the line starting with "disable!" +# - HOMEBREW_NO_INSTALL_FROM_API=1 brew install ruby@2 +# - /opt/homebrew/opt/ruby@2.7/bin/bundle config build.nokogiri --use-system-libraries +# - run this Makefile with `BUNDLE=/opt/homebrew/opt/ruby@2.7/bin/bundle make' +BUNDLE ?= bundle # RUBYOPT='-W:no-deprecated' does not work for some reason (https://stackoverflow.com/questions/60350374/cannot-suppress-ruby-2-7-0-warnings) -RUBY := RUBYOPT=-W0 bundle exec +RUBY := RUBYOPT=-W0 $(BUNDLE) exec JEKYLL := $(RUBY) jekyll JEKYLL_SRC_FILES_CONTENT := $(shell find $(JEKYLL_SRC_DIR)/{_wiki,_wiki_ui,_wiki_webui} -type f -not -name '_*' -not -name '*.png' -not -name '*.jpg' ) JEKYLL_SRC_FILES_MUSTACHE := $(shell find $(JEKYLL_SRC_DIR)/ -type f -name '_Sidebar.md') @@ -30,10 +38,10 @@ baseurl := $(call yaml_get,$(CONFIG_FILE),baseurl) .PHONY : all all : $(JEKYLL_DIR)/_site -ifneq (,$(findstring bundle exec,$(RUBY))) +ifneq (,$(findstring $(BUNDLE) exec,$(RUBY))) .SECONDARY : gems gems : Gemfile.lock - bundle install --path gems + $(BUNDLE) install --path gems else .PHONY : gems gems : @@ -186,15 +194,8 @@ $(MAVEN_DIR)/pom.xml : $(JEKYLL_SRC_DIR)/_data/versions.yml $(JEKYLL_SRC_DIR)/_d fi .PHONY : serve -serve : ws all - ws -d $(CURDIR)/$(JEKYLL_DIR)/_site - -.PHONY : ws -ws : - @if ! which $@ >/dev/null 2>/dev/null; then \ - echo "ws is not installed, install with 'npm install -g local-web-server'" 2>&1; \ - exit 1; \ - fi +serve : all + ruby -r webrick -e "s = WEBrick::HTTPServer.new(:Port => 8080, :DocumentRoot => '$(CURDIR)/$(JEKYLL_DIR)/_site'); trap('INT') { s.shutdown }; s.start" .PHONY : publish publish : all diff --git a/website/src/Download.md b/website/src/Download.md index c5fb30f73b..ddbbce5bd4 100644 --- a/website/src/Download.md +++ b/website/src/Download.md @@ -3,6 +3,8 @@ layout: default --- # Download + + For installation instructions see [Installation]({{site.baseurl}}/Get-Help/User-Guide/Installation/). diff --git a/website/src/_includes/download-link b/website/src/_includes/download-link index 5ec0b01226..371655c4f1 100644 --- a/website/src/_includes/download-link +++ b/website/src/_includes/download-link @@ -1,4 +1,4 @@ - + For {% case include.file.platform %} {% when 'windows' %} Windows diff --git a/website/src/_wiki/Get-Help/User-Guide.md b/website/src/_wiki/Get-Help/User-Guide.md index 756e9bb5c3..30763e4059 100644 --- a/website/src/_wiki/Get-Help/User-Guide.md +++ b/website/src/_wiki/Get-Help/User-Guide.md @@ -3,22 +3,35 @@ DAISY Pipeline 2 is an umbrella term for various things. - DAISY Pipeline 2 is a [desktop - application](https://github.com/daisy/pipeline-ui/wiki/Quick-Start-Guide/). + application](https://github.com/daisy/pipeline-ui/wiki/Quick-Start-Guide/) + for Windows and macOS. -- DAISY Pipeline 2 can also be run as a [service](Pipeline-as-Service) - to which client programs can connect, through a web API. +- It is a [web server](Pipeline-as-Service) to which client programs + can connect through a RESTful API. - The DAISY Pipeline 2 [web application](https://github.com/daisy/pipeline-webui/wiki/User-Guide/) is an example of such a server-client configuration. Unlike with the desktop application, multiple users can work together in the same - application, each through their own web browser, while the real work - happens on a central server. The web application offers some - additional features that are not available in the desktop - application yet, such as + application, each through their own web browser, while the actual + processing happens on a central server. The web application offers + some additional features that are not available in the desktop + application, such as "[templates](https://github.com/daisy/pipeline-webui/wiki/Templating/)". -- There is also a [command line tool](Command-Line). +- There is also a [command line interface](Command-Line). -DAISY Pipeline 2, in all its forms, is supported on Windows, Mac OS, -and Linux. +- DAISY Pipeline 2 is a [Java library](API-Documentation#java-api). + +- For Windows users, there is also the [Save As DAISY addin for MS + Word](https://daisy.org/activities/software/save-as-daisy-ms-word-add-in/), + which is becoming a DAISY Pipeline user interface specifically for + processing Word documents. + +- "DAISY Pipeline" could also refer to the predecessor of DAISY + Pipeline 2 which we now call "Pipeline 1". Because a lot of + organisations still rely on some Pipeline 1 converters in their + production today, we are currently working on making these legacy + converters available through the DAISY Pipeline 2 user and + programming interfaces, in order to make the transition to Pipeline + 2 smoother and to retire the Pipeline 1 user interface. diff --git a/website/src/_wiki/Get-Help/User-Guide/Installation.md b/website/src/_wiki/Get-Help/User-Guide/Installation.md index d672c0a2d3..6e6b082a99 100644 --- a/website/src/_wiki/Get-Help/User-Guide/Installation.md +++ b/website/src/_wiki/Get-Help/User-Guide/Installation.md @@ -13,22 +13,12 @@ These are the installation instructions for each package. Before starting with the installation make sure the [system requirements](#system-requirements) are fulfilled. -### Windows +### Windows and macOS -For Windows users there is the Windows installer. It installs the -desktop application. Launch the installer and follow the instructions -on the screen. The desktop application can also be used to run a -server. - -### Mac OS - -For Mac OS users there is the disk image. It contains the desktop -application. To install, open the image and drag the application file -to the "Applications" folder, or any other destination you want. - -Contents of the disc image - -The desktop application can also be used to run a server. +For Windows and macOS users we provide easy to use installers for +installing the desktop application. Launch the installer and follow +the instructions on the screen. The desktop application can also be +used to run a server. ### Debian @@ -178,5 +168,5 @@ To learn more about Docker see [https://docs.docker.com](https://docs.docker.com The standalone server requires a Java runtime environment. The minimum required version of Java is 11. We recommend installing Java from https://adoptium.net/. Users of the desktop application (Windows and -MacOS) do not have to worry about Java because it is included in the +macOS) do not have to worry about Java because it is included in the application. diff --git a/website/src/_wiki/Get-Help/User-Guide/Installation/disk-image.png b/website/src/_wiki/Get-Help/User-Guide/Installation/disk-image.png deleted file mode 100644 index b2691cc341e48fc311e745693b4cb05c8d9e8f57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 240768 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 MB98OE{Pv&!3;H%GZ~y=R diff --git a/website/src/_wiki/Support.md b/website/src/_wiki/Support.md new file mode 100644 index 0000000000..949b98b913 --- /dev/null +++ b/website/src/_wiki/Support.md @@ -0,0 +1,11 @@ +# Support our work + +

+ + +If you find DAISY Pipeline useful, please help us by donating to support its +ongoing maintenance. [Support out work](https://daisy.org/pipelineAppSponsor). diff --git a/website/src/_wiki/_Sidebar.md b/website/src/_wiki/_Sidebar.md index 0040e935dc..a2dc81ee98 100644 --- a/website/src/_wiki/_Sidebar.md +++ b/website/src/_wiki/_Sidebar.md @@ -52,9 +52,8 @@ * [[Troubleshooting Desktop Application|https://github.com/daisy/pipeline-ui/wiki/Troubleshooting]] * [[Forum|https://github.com/daisy/pipeline/discussions]] * [[Contact|mailto:daisy-pipeline@mail.daisy.org]] -* [[Contribute​|Contribute]] +* [[Contribute|Contribute]] * [[Source Code|https://github.com/daisy/pipeline]] - * [[Developers Mailing List|https://groups.google.com/forum/#!forum/daisy-pipeline-dev]] * [[Developer Guide|Developer-Guide]] * [[Source Code|Sources]] * [[Build System|Building]] @@ -65,4 +64,5 @@ * [[Debugging|Debugging]] * [[Release Management|Releasing]] * [[Easy Hacks|http://daisy.github.io/pipeline/Contribute/Easy-Hacks.html]] +* [[Support​|Support]] * [[DAISY Home|http://www.daisy.org]] diff --git a/website/src/index.md b/website/src/index.md index 8aa9686681..a30fbd7a20 100644 --- a/website/src/index.md +++ b/website/src/index.md @@ -4,55 +4,61 @@ title: Home --- # DAISY Pipeline 2 -The DAISY Pipeline 2 is an open-source, cross-platform framework for -the automated processing of digital content in and between various -file formats. It intends to facilitate the production and maintenance -of accessible content for people with print disabilities. +The DAISY Pipeline 2 is an open-source, cross-platform framework and +user interface for the automated processing of digital content in and +between various file formats. It intends to facilitate the production +and maintenance of accessible content for people with print +disabilities. ## Accessibility -The Pipeline was developed by and for the -[DAISY community](http://www.daisy.org/), a group of organizations -committed to making content accessible. It goes without saying that -accessiblity is the main interest of the tool. There are Pipeline -transformation for migrating from one accessible format to another, -enriching an input format with certain accessible features, and -producing formats targeting a specific disability. +The Pipeline was developed by and for the [DAISY +community](http://www.daisy.org/), a group of organizations committed +to making content accessible. It goes without saying that accessiblity +is the main interest of the tool. There are Pipeline transformation +for migrating from one accessible format to another, enriching an +input format with certain accessible features, and producing formats +targeting a specific disability. ## Standards -Accessibility goes hand in hand with standards. The file formats that -the Pipeline primarily focuses on are [EPUB 3](http://idpf.org/epub/301), -[DAISY](http://www.daisy.org/specifications) and -[PEF](http://pef-format.org/?page_id=15). +Accessibility goes hand in hand with standards. Notable file formats +that DAISY Pipeline evolves around are +[EPUB](https://www.w3.org/TR/epub-33/), +[DAISY](https://daisy.org/activities/standards/), +[PEF](https://braillespecs.github.io/pef/pef-specification.html) and +[eBraille](https://daisy.github.io/ebraille/). Standards are also important under the hood. The system is based on standard XML processing technologies, notably W3C recommendations like -XProc and XSLT 2.0, but also XPath 2.0, OASIS XML Catalogs, etc. These +XProc and XSLT 3.0, but also XPath 3.1, OASIS XML Catalogs, etc. These technologies are platform neutral, supported by active communities, and easy to maintain. ## Cross-platform -The application can be run on most common operating systems. It must -be run in a Java runtime environment, but its platform neutral RESTful -API (web service) allows it to be called from any programming language -and makes it interoperable with heterogenous production workflows. +The application can be run in some form on all common operating +systems. A desktop application is provided for Windows and macOS. In +addition, for Windows users, there is the [Save As DAISY addin for MS +Word](https://daisy.org/activities/software/save-as-daisy-ms-word-add-in/), +which is becoming a DAISY Pipeline user interface specifically for +processing Word documents. -In addition to the programming interface there is also a command-line -interface and there are graphical user interfaces. +DAISY Pipeline also has programming interfaces. When run as a daemon +(web server), there are no restrictions. The server can be run on any +OS, and its platform neutral RESTful API allows it to be called from +any programming language and makes it interoperable with heterogenous +production workflows. A command-line interface is available too. When +used as a Java library, there are no OS restictions either. ## Modular The system was designed with a modular architecture. Modularity is the -key to a better scalability and extensibility. Processing -functionality is provided by cohesive and reusable software -components, called "modules". The runtime framework is backed by a -service-oriented and dynamic module system (OSGi). +key to a better scalability and extensibility. ## Collaborative -The project is led and maintained by the DAISY Consortium but involves +The project is led and maintained by the DAISY Consortium and involves several member organizations. This reduces the duplication of effort and ensures maximum sharing of best practices among the user community. @@ -65,10 +71,6 @@ under a business-friendly licence stimulate collaboration between organizations and to maximize reuse and integration in other contexts, including commercial software. - - Find out how you can [join the community](Contribute).
@@ -86,11 +88,12 @@ Pipeline 2 and find out [how it works](Get-Help). ## Background -The DAISY Pipeline is a collaborative project maintained by the -[DAISY Consortium](http://www.daisy.org/), with numerous organizations +The DAISY Pipeline is a collaborative project maintained by the [DAISY +Consortium](http://www.daisy.org/), with numerous organizations participating and contributing to the development. The DAISY Pipeline -2 project is the follow-up of the -[DAISY Pipeline 1](http://www.daisy.org/pipeline) project. +2 project is the follow-up of the original [DAISY +Pipeline](https://daisy.org/info-help/document-archive/archived-projects/pipeline-1/) +(now referred to as "Pipeline 1") project. The initial DAISY Pipeline project was started in 2006. Since then, new standards and technologies have emerged and have been embraced in @@ -115,9 +118,11 @@ those standards), version 2 of the project aims to - increase interoperability with the heterogeneous production workflows - increase the likelihood of re-use in both open source and commercial applications. - - - +Because a lot of organisations still rely on some Pipeline 1 +converters in their production today, we are currently working on +making these legacy converters available through the DAISY Pipeline 2 +user and programming interfaces, in order to make the transition to +Pipeline 2 smoother and to retire the Pipeline 1 user interface. @@ -110,7 +110,7 @@ org.daisy.pipeline.modules.braille pef-utils - 6.0.1 + 6.0.2-SNAPSHOT org.daisy.pipeline.modules.braille @@ -124,47 +124,47 @@ org.daisy.pipeline.modules common-utils - 3.1.1 + 3.1.2-SNAPSHOT org.daisy.pipeline.modules file-utils - 4.3.0 + 4.3.1-SNAPSHOT org.daisy.pipeline.modules fileset-utils - 6.1.3 + 6.1.4-SNAPSHOT org.daisy.pipeline.modules html-utils - 6.4.0 + 6.4.1-SNAPSHOT org.daisy.pipeline.modules image-utils - 1.0.7 + 1.0.8-SNAPSHOT org.daisy.pipeline.modules mathml-utils - 1.0.2 + 1.0.3-SNAPSHOT org.daisy.pipeline.modules mediatype-utils - 2.0.1 + 2.0.2-SNAPSHOT org.daisy.pipeline.modules validation-utils - 2.0.0 + 2.0.1-SNAPSHOT org.daisy.pipeline.modules zip-utils - 2.1.7 + 2.1.8-SNAPSHOT @@ -212,67 +212,67 @@ org.daisy.pipeline.modules ace-adapter - 1.0.8 + 1.0.9-SNAPSHOT org.daisy.pipeline.modules asciimath-utils - 1.0.10 + 1.0.11-SNAPSHOT org.daisy.pipeline.modules css-utils - 5.4.0 + 6.0.0-SNAPSHOT org.daisy.pipeline.modules daisy202-utils - 1.6.3 + 1.6.4-SNAPSHOT org.daisy.pipeline.modules daisy3-utils - 4.1.5 + 4.1.6-SNAPSHOT org.daisy.pipeline.modules dtbook-utils - 5.1.0 + 5.1.1-SNAPSHOT org.daisy.pipeline.modules epub-utils - 2.2.0 + 2.2.1-SNAPSHOT org.daisy.pipeline.modules epubcheck-adapter - 1.1.12 + 1.1.13-SNAPSHOT org.daisy.pipeline.modules epub3-to-html - 1.0.0 + 1.0.1-SNAPSHOT org.daisy.pipeline.modules smil-utils - 4.0.1 + 4.0.2-SNAPSHOT org.daisy.pipeline.modules metadata-utils - 2.0.1 + 2.0.2-SNAPSHOT org.daisy.pipeline.modules odf-utils - 1.0.3 + 1.0.4-SNAPSHOT org.daisy.pipeline.modules zedai-utils - 1.2.1 + 1.2.2-SNAPSHOT - - - - - diff --git a/modules/braille/braille-common/src/main/resources/xml/abstract-block-translator.xsl b/modules/braille/braille-common/src/main/resources/xml/abstract-block-translator.xsl index 0e92b73b40..2936ceac7e 100644 --- a/modules/braille/braille-common/src/main/resources/xml/abstract-block-translator.xsl +++ b/modules/braille/braille-common/src/main/resources/xml/abstract-block-translator.xsl @@ -2,6 +2,8 @@ @@ -21,94 +23,81 @@ + + + + - - - - - - - - + + + + - + - - - - + + + + + + + + - - - - + + + + + + + + "/> + + + + + + + - - - - - - - - + + + + + + + + "/> + + + - - - - - - + + + + + - - - - - - - + + - - - - - - - - - - - - - - - - - @@ -119,7 +108,9 @@ - + + + @@ -135,12 +126,6 @@ - - - - @@ -150,9 +135,10 @@ - - + + + @@ -165,350 +151,119 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - + + + + + + + + - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - string() function not supported in string-set property - - - - counter() function not supported in string-set property - - - - target-text() function not supported in string-set property - - - - target-string() function not supported in string-set property - - - - target-counter() function not supported in string-set property - - - - target-content() function not supported in string-set property - - - - leader() function not supported in string-set property - - - - () function not supported in string-set property - - - - - - - + - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - + + + + + - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - + + + + + + + + "/> + + + + + + + + + - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - @@ -516,47 +271,31 @@ - - - + + + + + + + + + + + + - - - - - - - - - - - - <_> - - - - - - - - - - - - + + - + <_> + + + + @@ -564,75 +303,4 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <_/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/modules/braille/braille-common/src/main/resources/xml/block-translator-from-text-transform.xsl b/modules/braille/braille-common/src/main/resources/xml/block-translator-from-text-transform.xsl index 7babe2c2ef..0e00bd9328 100644 --- a/modules/braille/braille-common/src/main/resources/xml/block-translator-from-text-transform.xsl +++ b/modules/braille/braille-common/src/main/resources/xml/block-translator-from-text-transform.xsl @@ -2,65 +2,24 @@ - + - + + - + - - - + + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/modules/braille/braille-common/src/main/resources/xml/block-translator.xpl b/modules/braille/braille-common/src/main/resources/xml/block-translator.xpl index 8c64113a3b..a30fc78884 100644 --- a/modules/braille/braille-common/src/main/resources/xml/block-translator.xpl +++ b/modules/braille/braille-common/src/main/resources/xml/block-translator.xpl @@ -1,19 +1,31 @@ - + exclude-inline-prefixes="#all" + type="px:block-translate"> - + + + px:css-parse-properties + + - + + + Make css:before and css:after elements from pseudo-element rules. + + + + + + + - + @@ -21,4 +33,13 @@ + + + Convert css:before and css:after elements back to pseudo-element rules. + + + + + + diff --git a/modules/braille/braille-common/src/main/resources/xml/collapse-pseudo-elements.xsl b/modules/braille/braille-common/src/main/resources/xml/collapse-pseudo-elements.xsl new file mode 100644 index 0000000000..3b44816cea --- /dev/null +++ b/modules/braille/braille-common/src/main/resources/xml/collapse-pseudo-elements.xsl @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/braille/braille-common/src/main/resources/xml/expand-pseudo-elements.xsl b/modules/braille/braille-common/src/main/resources/xml/expand-pseudo-elements.xsl new file mode 100644 index 0000000000..dc4e309dcc --- /dev/null +++ b/modules/braille/braille-common/src/main/resources/xml/expand-pseudo-elements.xsl @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_> + + + + + + + + + + + + + + + + diff --git a/modules/braille/braille-common/src/main/resources/xml/library.xsl b/modules/braille/braille-common/src/main/resources/xml/library.xsl index 39b26c2192..7c91870b4c 100644 --- a/modules/braille/braille-common/src/main/resources/xml/library.xsl +++ b/modules/braille/braille-common/src/main/resources/xml/library.xsl @@ -1,8 +1,7 @@ + xmlns:pf="http://www.daisy.org/ns/pipeline/functions"> - + + + + + org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules @@ -68,8 +68,8 @@ org.daisy.pipeline.modules.impl.Module_braille_css_utils, org.daisy.pipeline.braille.css.impl.BrailleCssCascader, - org.daisy.pipeline.braille.css.calabash.impl.CssShiftIdStep$Provider, - org.daisy.pipeline.braille.css.calabash.impl.CssShiftStringSetStep$Provider, + org.daisy.pipeline.braille.css.saxon.impl.StyleFunctionProvider, + org.daisy.pipeline.braille.css.saxon.impl.StyledTextFunctionProvider, org.daisy.pipeline.braille.css.saxon.impl.ParseStylesheetDefinition, org.daisy.pipeline.braille.css.saxon.impl.RenderTableByDefinition @@ -112,7 +112,8 @@ - org.daisy.pipeline.modules.impl.Module_test_module + org.daisy.pipeline.modules.impl.Module_test_module, + MockCSSExtension diff --git a/modules/braille/braille-css-utils/src/main/README.md b/modules/braille/braille-css-utils/src/main/README.md index 0de9e39bba..969ec12aef 100644 --- a/modules/braille/braille-css-utils/src/main/README.md +++ b/modules/braille/braille-css-utils/src/main/README.md @@ -23,21 +23,11 @@ of medium "print" is supported though, namely the properties color, font-style, font-weight, text-decoration. -## XMLCalabash XProc steps ([`org.daisy.common.xproc.calabash.XProcStepProvider`](http://daisy.github.io/pipeline/api/org/daisy/common/xproc/calabash/XProcStepProvider.html)) - -- [`{http://www.daisy.org/ns/pipeline/braille-css}shift-id`](java/org/daisy/pipeline/braille/css/calabash/impl/CssShiftIdStep.java) - - Move css:id attributes to inline boxes, see [XProc documentation](resources/xml/shift-id.xpl) - -- [`{http://www.daisy.org/ns/pipeline/braille-css}shift-string-set`](java/org/daisy/pipeline/braille/css/calabash/impl/CssShiftStringSetStep.java) - - Move 'string-set' declarations to inline boxes, see [XProc documentation](resources/xml/shift-string-set.xpl) - ## Saxon XPath functions ([`net.sf.saxon.lib.ExtensionFunctionDefinition`](https://www.saxonica.com/html/documentation9.8/javadoc/net/sf/saxon/lib/ExtensionFunctionDefinition.html)) - [`{http://www.daisy.org/ns/pipeline/braille-css}parse-stylesheet`](java/org/daisy/pipeline/braille/css/saxon/impl/ParseStylesheetDefinition.java) - Parse a style sheet, see [XSLT documentation](resources/xml/base.xsl) + Parse a style sheet, see [XSLT documentation](resources/xml/library.xsl) - [`{http://www.daisy.org/ns/pipeline/braille-css}render-table-by`](java/org/daisy/pipeline/braille/css/saxon/impl/RenderTableByDefinition.java) @@ -46,8 +36,6 @@ - - diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/CSSStyledText.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/CSSStyledText.java index b9e1c4ff99..e2ef0b1e6a 100644 --- a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/CSSStyledText.java +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/CSSStyledText.java @@ -1,31 +1,24 @@ package org.daisy.pipeline.braille.css; -import java.util.function.Function; import java.util.HashMap; import java.util.Locale; import java.util.Map; -import java.lang.reflect.InvocationTargetException; -import org.daisy.braille.css.InlineStyle; -import org.daisy.braille.css.InlineStyle.RuleMainBlock; -import org.daisy.braille.css.RuleTextTransform; import org.daisy.braille.css.SimpleInlineStyle; - -import cz.vutbr.web.css.RuleBlock; +import org.daisy.pipeline.braille.css.impl.BrailleCssSerializer; /** * Note that CSSStyledText objects are not immutable because {@link SimpleInlineStyle} - * is mutable (due to {@link SimpleInlineStyle#removeProperty(String)} and {@link - * SimpleInlineStyle#iterator()} methods). + * is mutable (due to the {@link SimpleInlineStyle#removeProperty(String)} and {@link + * SimpleInlineStyle#iterator()} methods and because the {@link cz.vutbr.web.css.Term} and {@link + * cz.vutbr.web.css.Declaration} objects are not immutable). */ public class CSSStyledText implements Cloneable { private final String text; private final Locale language; private Map textAttributes; - private Style style; - - private static final Function parseCSS = memoize(Style::parse); + private SimpleInlineStyle style; public CSSStyledText(String text, SimpleInlineStyle style) { this(text, style, null, null); @@ -41,30 +34,7 @@ public CSSStyledText(String text, SimpleInlineStyle style, Map te public CSSStyledText(String text, SimpleInlineStyle style, Locale language, Map textAttributes) { this.text = text; - this.style = new Style(); - this.style.properties = style; - this.language = language; - this.textAttributes = textAttributes; - } - - public CSSStyledText(String text, String style) { - this(text, style, null, null); - } - - public CSSStyledText(String text, String style, Locale language) { - this(text, style, language, null); - } - - public CSSStyledText(String text, String style, Map textAttributes) { - this(text, style, null, textAttributes); - } - - public CSSStyledText(String text, String style, Locale language, Map textAttributes) { - this.text = text; - if (style == null) - this.style = null; - else - this.style = parseCSS.apply(style); + this.style = style; this.language = language; this.textAttributes = textAttributes; } @@ -81,26 +51,7 @@ public String getText() { } public SimpleInlineStyle getStyle() { - if (style == null) - return null; - else - return style.properties; - } - - // currently not used - public RuleTextTransform getDefaultTextTransformDefinition() { - if (style == null) - return null; - else - return style.defaultTextTransformDef; - } - - // currently not used - public RuleTextTransform getTextTransformDefinition(String name) { - if (style == null || style.textTransformDefs == null) - return null; - else - return style.textTransformDefs.get(name); + return style; } public Locale getLanguage() { @@ -121,7 +72,7 @@ public CSSStyledText clone() { } } if (style != null) - clone.style = (Style)style.clone(); + clone.style = (SimpleInlineStyle)style.clone(); if (textAttributes != null) clone.textAttributes = new HashMap(textAttributes); return clone; @@ -130,8 +81,8 @@ public CSSStyledText clone() { @Override public String toString() { String s = text; - if (style != null && style.properties != null && !style.properties.isEmpty()) - s += "{" + style.properties + "}"; + if (style != null && !style.isEmpty()) + s += "{" + BrailleCssSerializer.serializeDeclarationList(style) + "}"; if (language != null && !"und".equals(language.toLanguageTag())) s += "{" + language.toLanguageTag() + "}"; return s; @@ -158,12 +109,12 @@ public boolean equals(Object other) { return false; } else if (that.textAttributes != null && !that.textAttributes.isEmpty()) return false; - if (this.style != null && this.style.properties != null && !this.style.properties.isEmpty()) { - if (that.style == null || that.style.properties == null || that.style.properties.isEmpty()) + if (this.style != null && !this.style.isEmpty()) { + if (that.style == null || that.style.isEmpty()) return false; if (!this.style.equals(that.style)) return false; - } else if (that.style != null && that.style.properties != null && !that.style.properties.isEmpty()) + } else if (that.style != null && !that.style.isEmpty()) return false; return true; } @@ -177,99 +128,4 @@ public int hashCode() { hash = prime * hash + (style == null ? 0 : style.hashCode()); return hash; } - - private static class Style implements Cloneable { - - SimpleInlineStyle properties; - RuleTextTransform defaultTextTransformDef; - Map textTransformDefs; - - @Override - public Object clone() { - Style clone; { - try { - clone = (Style)super.clone(); - } catch (CloneNotSupportedException e) { - throw new InternalError("coding error"); - } - } - if (properties != null) - clone.properties = (SimpleInlineStyle)properties.clone(); - return clone; - } - - @Override - // FIXME: don't ignore text-transform defs - public boolean equals(Object other) { - if (this == other) - return true; - if (!(other instanceof Style)) - return false; - Style that = (Style)other; - if (this.properties != null) { - if (that.properties == null) - return false; - if (!this.properties.equals(that.properties)) - return false; - } else if (that.properties != null) - return false; - return true; - } - - @Override - public int hashCode() { - final int prime = 31; - int hash = 1; - hash = prime * hash + (properties == null ? 0 : properties.hashCode()); - return hash; - } - - public static Style parse(String style) { - InlineStyle inlineStyle = new InlineStyle(style); - Style s = new Style(); - s.properties = new SimpleInlineStyle(inlineStyle.getMainStyle()); - for (RuleBlock b : inlineStyle) - if (b instanceof RuleMainBlock) {} // already handled - else if (b instanceof RuleTextTransform) { - RuleTextTransform def = (RuleTextTransform)b; - String name = def.getName(); - if (name != null) { - if (s.textTransformDefs == null) - s.textTransformDefs = new HashMap(); - s.textTransformDefs.put(name, def); - } else - s.defaultTextTransformDef = def; - } else - throw new RuntimeException("Unexpected style: " + b); - return s; - } - } - - private static Function memoize(Function function) { - Map cache = new HashMap(); - return new Function() { - public V apply(K key) { - V value; - if (cache.containsKey(key)) - value = cache.get(key); - else { - value = function.apply(key); - if (value != null) - cache.put(key, value); } - if (value == null) - return null; - else { - try { - return (V)value.getClass().getMethod("clone").invoke(value); } - catch (IllegalAccessException - | IllegalArgumentException - | InvocationTargetException - | NoSuchMethodException - | SecurityException e) { - throw new RuntimeException("Could not invoke clone() method", e); - } - } - } - }; - } } diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/CounterFunction.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/CounterFunction.java new file mode 100644 index 0000000000..15c083578a --- /dev/null +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/CounterFunction.java @@ -0,0 +1,20 @@ +package org.daisy.pipeline.braille.css; + +import java.util.Optional; + +import cz.vutbr.web.css.Term; + +import org.daisy.pipeline.css.CounterStyle; + +/** + * A {@code counter()} function. See Printing + * Counters in the braille CSS specification. + */ +public interface CounterFunction extends Term { + + public String getCounter(); + + public Optional getStyle(); + +} diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/CounterSet.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/CounterSet.java new file mode 100644 index 0000000000..f566d96122 --- /dev/null +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/CounterSet.java @@ -0,0 +1,11 @@ +package org.daisy.pipeline.braille.css; + +import cz.vutbr.web.css.TermPair; + +/** + * A counter name-value pair. See Manipulating + * Counters in the braille CSS specification. + */ +public interface CounterSet extends TermPair { +} diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/StyleTransformer.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/StyleTransformer.java new file mode 100644 index 0000000000..4db78a4df4 --- /dev/null +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/StyleTransformer.java @@ -0,0 +1,26 @@ +package org.daisy.pipeline.braille.css; + +import org.daisy.braille.css.SimpleInlineStyle; +import org.daisy.braille.css.SupportedBrailleCSS; +import org.daisy.pipeline.braille.css.impl.StyleTransformerImpl; +import org.daisy.pipeline.braille.css.xpath.Style; + +/** + * Transform a style to a different model. + */ +public interface StyleTransformer { + + /** + * @param fromSupportedBrailleCSS input model + * @param toSupportedBrailleCSS output model + */ + public static StyleTransformer of(SupportedBrailleCSS fromSupportedBrailleCSS, + SupportedBrailleCSS toSupportedBrailleCSS) { + return new StyleTransformerImpl(fromSupportedBrailleCSS, toSupportedBrailleCSS); + } + + public SimpleInlineStyle transform(SimpleInlineStyle style); + + public Style transform(Style style); + +} diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/TextStyleParser.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/TextStyleParser.java new file mode 100644 index 0000000000..1590457242 --- /dev/null +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/TextStyleParser.java @@ -0,0 +1,27 @@ +package org.daisy.pipeline.braille.css; + +import org.daisy.braille.css.SimpleInlineStyle; + +import org.daisy.pipeline.braille.css.impl.BrailleCssParser; + +/** + * Parse string to {@link SimpleInlineStyle} for use in {@link CSSStyledText}. + */ +public interface TextStyleParser { + + public SimpleInlineStyle parse(String style); + + /** + * Parse a style and inherit from a parent style. Using this method, rather than using {@link + * #parse(String)} and calling {@link SimpleInlineStyle#inheritFrom} on the result, allows for + * caching to happen. + * + * As opposed to {@link #parse(String)}, this method always concretizes "inherit" values (even + * if the provided parent style is {@code null} or empty). + */ + public SimpleInlineStyle parse(String style, SimpleInlineStyle parent); + + public static TextStyleParser getInstance() { + return BrailleCssParser.getInstance(); + } +} diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/BrailleCssCascader.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/BrailleCssCascader.java index 5522a6ebf2..4df821b179 100644 --- a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/BrailleCssCascader.java +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/BrailleCssCascader.java @@ -13,6 +13,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.Set; @@ -25,7 +26,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; -import cz.vutbr.web.css.CSSProperty; import cz.vutbr.web.css.Declaration; import cz.vutbr.web.css.NodeData; import cz.vutbr.web.css.Rule; @@ -39,12 +39,14 @@ import cz.vutbr.web.css.TermIdent; import cz.vutbr.web.css.TermURI; import cz.vutbr.web.csskit.antlr.CSSParserFactory; +import cz.vutbr.web.csskit.DeclarationImpl; import cz.vutbr.web.csskit.RuleFactoryImpl; +import cz.vutbr.web.csskit.TermURIImpl; import cz.vutbr.web.domassign.DeclarationTransformer; -import org.daisy.braille.css.AnyAtRule; -import org.daisy.braille.css.BrailleCSSDeclarationTransformer; +import org.daisy.braille.css.BrailleCSSExtension; import org.daisy.braille.css.BrailleCSSParserFactory; +import org.daisy.braille.css.BrailleCSSParserFactory.Context; import org.daisy.braille.css.BrailleCSSProperty; import org.daisy.braille.css.BrailleCSSRuleFactory; import org.daisy.braille.css.RuleCounterStyle; @@ -53,11 +55,12 @@ import org.daisy.braille.css.RuleVolume; import org.daisy.braille.css.RuleVolumeArea; import org.daisy.braille.css.SelectorImpl.PseudoElementImpl; -import org.daisy.braille.css.SimpleInlineStyle; import org.daisy.braille.css.SupportedBrailleCSS; +import org.daisy.braille.css.VendorAtRule; import org.daisy.common.file.URLs; import org.daisy.common.transform.XMLTransformer; import org.daisy.pipeline.braille.css.SupportedPrintCSS; +import org.daisy.pipeline.braille.css.impl.BrailleCssParser; import org.daisy.pipeline.braille.css.impl.BrailleCssSerializer; import org.daisy.pipeline.css.CssCascader; import org.daisy.pipeline.css.CssPreProcessor; @@ -65,7 +68,14 @@ import org.daisy.pipeline.css.Medium; import org.daisy.pipeline.css.XsltProcessor; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -76,6 +86,8 @@ ) public class BrailleCssCascader implements CssCascader { + private static final Logger logger = LoggerFactory.getLogger(BrailleCssCascader.class); + /** * Note that this implementation only supports a very small subset of medium "print", namely the * properties color, font-style, font-weight, text-decoration. @@ -106,7 +118,7 @@ public XMLTransformer newInstance(Medium medium, case EMBOSSED: case BRAILLE: return new Transformer(uriResolver, preProcessor, xsltProcessor, userAndUserAgentStylesheets, medium, attributeName, - brailleParserFactory, brailleRuleFactory, brailleCSS, brailleDeclarationTransformer); + brailleParserFactory, brailleRuleFactory, brailleCSS, brailleCSS); case PRINT: return new Transformer(uriResolver, preProcessor, xsltProcessor, userAndUserAgentStylesheets, medium, attributeName, printParserFactory, printRuleFactory, printCSS, printDeclarationTransformer); @@ -122,13 +134,51 @@ public XMLTransformer newInstance(Medium medium, private static final CSSParserFactory printParserFactory = CSSParserFactory.getInstance(); // medium braille/embossed - private static final SupportedCSS brailleCSS = new SupportedBrailleCSS(false, true); - private static final DeclarationTransformer brailleDeclarationTransformer - = new BrailleCSSDeclarationTransformer(brailleCSS); - private static final RuleFactory brailleRuleFactory = new BrailleCSSRuleFactory(); - private static final CSSParserFactory brailleParserFactory = new BrailleCSSParserFactory(); + private final List brailleCSSExtensions = new ArrayList<>(); + private SupportedBrailleCSS brailleCSS = null; + private BrailleCSSRuleFactory brailleRuleFactory = null; + private BrailleCSSParserFactory brailleParserFactory = null; + private BrailleCssParser brailleCSSParser = null; + + + @Reference( + name = "BrailleCSSExtension", + unbind = "-", + service = BrailleCSSExtension.class, + cardinality = ReferenceCardinality.MULTIPLE, + policy = ReferencePolicy.STATIC + ) + protected void addBrailleCSSExtension(BrailleCSSExtension x) { + logger.debug("Binding BrailleCSSExtension: {}", x); + brailleCSSExtensions.add(x); + } - private static class Transformer extends JStyleParserCssCascader { + @Activate + protected void init() { + boolean allowUnknownVendorExtensions = false; + brailleCSS = new SupportedBrailleCSS(false, true, brailleCSSExtensions, allowUnknownVendorExtensions); + brailleRuleFactory = new BrailleCSSRuleFactory(brailleCSSExtensions, allowUnknownVendorExtensions); + brailleParserFactory = new BrailleCSSParserFactory(brailleRuleFactory); + brailleCSSParser = new BrailleCssParser() { + @Override + public BrailleCSSParserFactory getBrailleCSSParserFactory() { + return brailleParserFactory; + } + @Override + public Optional getSupportedBrailleCSS(Context context) { + switch (context) { + case ELEMENT: + case PAGE: + case VOLUME: + return Optional.of(brailleCSS); + default: + return Optional.empty(); + } + } + }; + } + + private class Transformer extends JStyleParserCssCascader { private final QName attributeName; private final boolean isBrailleCss; @@ -148,7 +198,7 @@ private Transformer(URIResolver resolver, CssPreProcessor preProcessor, XsltProc private Iterable textTransformRules = null; private Iterable hyphenationResourceRules = null; private Iterable counterStyleRules = null; - private Iterable otherAtRules = null; + private Iterable>> otherAtRules = null; protected Map serializeStyle(NodeData mainStyle, Map pseudoStyles, Element context) { if (isBrailleCss && pageRules == null) { @@ -188,7 +238,7 @@ protected Map serializeStyle(NodeData mainStyle, Map>>)(Iterable)Iterables.filter(styleSheet, VendorAtRule.class); } StringBuilder style = new StringBuilder(); if (mainStyle != null) @@ -202,11 +252,11 @@ protected Map serializeStyle(NodeData mainStyle, Map pageRule = getPageRule(mainStyle, pageRules); if (pageRule != null) { - insertPageStyle(style, pageRule, true); } + insertPageStyle(style, pageRule); } else if (isRoot) { pageRule = getPageRule("auto", pageRules); if (pageRule != null) - insertPageStyle(style, pageRule, true); } + insertPageStyle(style, pageRule); } if (isRoot) { Map volumeRule = getVolumeRule("auto", volumeRules); if (volumeRule != null) @@ -217,15 +267,16 @@ else if (isRoot) { insertHyphenationResourceDefinition(style, r); for (RuleCounterStyle r : counterStyleRules) insertCounterStyleDefinition(style, r); - for (AnyAtRule r : otherAtRules) { + for (VendorAtRule> r : otherAtRules) { if (style.length() > 0 && !style.toString().endsWith("} ")) { style.insert(0, "{ "); style.append("} "); } insertAtRule(style, r); }}} - if (!style.toString().replaceAll("\\s+", "").isEmpty()) - return ImmutableMap.of(attributeName, style.toString().trim()); - else + if (style.toString().trim().isEmpty()) return null; + if (style.length() > 1 && style.substring(style.length() - 2).equals("; ")) + style.delete(style.length() - 2, style.length()); + return ImmutableMap.of(attributeName, style.toString().trim()); } protected String serializeValue(Term value) { @@ -256,20 +307,17 @@ public int compare(PseudoElement e1, PseudoElement e2) { } }; - // FIXME: move parts of this to BrailleCssSerializer + // FIXME: make more use of BrailleCssSerializer private static void insertStyle(StringBuilder builder, NodeData nodeData) { - List keys = new ArrayList(nodeData.getPropertyNames()); - keys.remove("page"); - Collections.sort(keys); - for (String key : keys) { - Term value = nodeData.getValue(key, false); - if (value != null) - builder.append(key).append(": ").append(BrailleCssSerializer.toString(value)).append("; "); - else { - CSSProperty prop = nodeData.getProperty(key, false); - if (prop != null) // can be null for unspecified inherited properties - builder.append(key).append(": ").append(prop).append("; "); }} + List properties = new ArrayList(nodeData.getPropertyNames()); + properties.remove("page"); + Collections.sort(properties); + for (String prop : properties) { + String val = BrailleCssSerializer.serializePropertyValue(nodeData, prop, false); + if (val != null) // can be null for unspecified inherited properties + builder.append(prop).append(": ").append(val).append("; "); + } } private static void pseudoElementToString(StringBuilder builder, PseudoElement elem) { @@ -288,51 +336,29 @@ private static void pseudoElementToString(StringBuilder builder, PseudoElement e builder.append("(").append(s).append(")"); }} } - private static void insertPseudoStyle(StringBuilder builder, NodeData nodeData, PseudoElement elem, - Map> pageRules) { + private void insertPseudoStyle(StringBuilder builder, NodeData nodeData, PseudoElement elem, + Map> pageRules) { pseudoElementToString(builder, elem); builder.append(" { "); insertStyle(builder, nodeData); Map pageRule = getPageRule(nodeData, pageRules); if (pageRule != null) - insertPageStyle(builder, pageRule, false); + insertPageStyle(builder, pageRule); + if (builder.substring(builder.length() - 2).equals("; ")) + builder.replace(builder.length() - 2, builder.length(), " "); builder.append("} "); } - private static void insertPageStyle(StringBuilder builder, Map pageRule, boolean topLevel) { + private void insertPageStyle(StringBuilder builder, Map pageRule) { for (RulePage r : pageRule.values()) - insertPageStyle(builder, r, topLevel); + insertPageStyle(builder, r); } - private static void insertPageStyle(StringBuilder builder, RulePage pageRule, boolean topLevel) { - builder.append("@page"); - String pseudo = pageRule.getPseudo(); - if (pseudo != null && !"".equals(pseudo)) - builder.append(":").append(pseudo); - builder.append(" { "); - for (Declaration decl : Iterables.filter(pageRule, Declaration.class)) - insertDeclaration(builder, decl); - for (RuleMargin margin : Iterables.filter(pageRule, RuleMargin.class)) - insertMarginStyle(builder, margin); - builder.append("} "); - } - - private static void insertMarginStyle(StringBuilder builder, RuleMargin ruleMargin) { - builder.append("@").append(ruleMargin.getMarginArea()).append(" { "); - insertStyle(builder, new SimpleInlineStyle(ruleMargin, null, brailleDeclarationTransformer, brailleCSS)); - builder.append("} "); - } - - private static void insertDeclaration(StringBuilder builder, Declaration decl) { - StringBuilder s = new StringBuilder(); - Iterator> it = decl.iterator(); - while (it.hasNext()) { - s.append(BrailleCssSerializer.toString(it.next())); - if (it.hasNext()) s.append(" "); } - builder.append(decl.getProperty()).append(": ").append(s).append("; "); + private void insertPageStyle(StringBuilder builder, RulePage pageRule) { + builder.append(BrailleCssSerializer.toString(pageRule, brailleCSSParser)).append(" "); } - private static Map getPageRule(NodeData nodeData, Map> pageRules) { + private Map getPageRule(NodeData nodeData, Map> pageRules) { BrailleCSSProperty.Page pageProperty; { if (nodeData != null) pageProperty = nodeData.getProperty("page", false); @@ -354,7 +380,7 @@ private static Map getPageRule(NodeData nodeData, Map getPageRule(String name, Map> pageRules) { + private Map getPageRule(String name, Map> pageRules) { Map auto = pageRules == null ? null : pageRules.get("auto"); Map named = null; if (!name.equals("auto")) @@ -386,7 +412,7 @@ private static Map getPageRule(String name, Map from) { + private RulePage makePageRule(String name, String pseudo, List from) { RulePage pageRule = brailleRuleFactory.createPage().setName(name).setPseudo(pseudo); for (RulePage f : from) for (Rule r : f) @@ -424,28 +450,32 @@ private static RuleMargin getRuleMargin(Collection> rule, Stri return null; } - private static void insertVolumeStyle(StringBuilder builder, Map volumeRule, Map> pageRules) { + private void insertVolumeStyle(StringBuilder builder, Map volumeRule, Map> pageRules) { for (Map.Entry r : volumeRule.entrySet()) insertVolumeStyle(builder, r, pageRules); } - private static void insertVolumeStyle(StringBuilder builder, Map.Entry volumeRule, Map> pageRules) { + private void insertVolumeStyle(StringBuilder builder, Map.Entry volumeRule, Map> pageRules) { builder.append("@volume"); String pseudo = volumeRule.getKey(); if (pseudo != null && !"".equals(pseudo)) builder.append(":").append(pseudo); builder.append(" { "); - for (Declaration decl : Iterables.filter(volumeRule.getValue(), Declaration.class)) - insertDeclaration(builder, decl); + String declarations = BrailleCssSerializer.serializeDeclarationList(Iterables.filter(volumeRule.getValue(), Declaration.class)); + if (!declarations.isEmpty()) + builder.append(declarations).append("; "); for (RuleVolumeArea volumeArea : Iterables.filter(volumeRule.getValue(), RuleVolumeArea.class)) insertVolumeAreaStyle(builder, volumeArea, pageRules); + if (builder.substring(builder.length() - 2).equals("; ")) + builder.replace(builder.length() - 2, builder.length(), " "); builder.append("} "); } - private static void insertVolumeAreaStyle(StringBuilder builder, RuleVolumeArea ruleVolumeArea, Map> pageRules) { + private void insertVolumeAreaStyle(StringBuilder builder, RuleVolumeArea ruleVolumeArea, Map> pageRules) { builder.append("@").append(ruleVolumeArea.getVolumeArea().value).append(" { "); StringBuilder innerStyle = new StringBuilder(); Map pageRule = null; + List declarations = new ArrayList<>(); for (Declaration decl : Iterables.filter(ruleVolumeArea, Declaration.class)) if ("page".equals(decl.getProperty())) { StringBuilder s = new StringBuilder(); @@ -455,9 +485,13 @@ private static void insertVolumeAreaStyle(StringBuilder builder, RuleVolumeArea if (it.hasNext()) s.append(" "); } pageRule = getPageRule(s.toString(), pageRules); } else - insertDeclaration(innerStyle, decl); + declarations.add(decl); + if (!declarations.isEmpty()) + innerStyle.append(BrailleCssSerializer.serializeDeclarationList(declarations)).append("; "); if (pageRule != null) - insertPageStyle(innerStyle, pageRule, false); + insertPageStyle(innerStyle, pageRule); + if (innerStyle.length() > 1 && innerStyle.substring(innerStyle.length() - 2).equals("; ")) + innerStyle.replace(innerStyle.length() - 2, innerStyle.length(), " "); builder.append(innerStyle).append("} "); } @@ -466,9 +500,10 @@ private static void insertTextTransformDefinition(StringBuilder builder, RuleTex String name = rule.getName(); if (name != null) builder.append(' ').append(name); builder.append(" { "); - for (Declaration decl : rule) { - if (decl.size() == 1 && decl.get(0) instanceof TermURI) { - TermURI term = (TermURI)decl.get(0); + List declarationList = new ArrayList<>(); + for (Declaration d : rule) { + if (d.size() == 1 && d.get(0) instanceof TermURI) { + TermURI term = (TermURI)d.get(0); URI uri = URLs.asURI(term.getValue()); if (!uri.isAbsolute() && !uri.getSchemeSpecificPart().startsWith("/")) { // relative resource: make absolute and convert to "volatile-file" URI to bypass @@ -485,12 +520,14 @@ private static void insertTextTransformDefinition(StringBuilder builder, RuleTex } catch (IllegalArgumentException e) { // not a file URI } + d = createDeclaration(d.getProperty(), createTermURI(uri)); } - builder.append(decl.getProperty()).append(": ").append("url(\"" + uri + "\")").append("; "); - continue; } - insertDeclaration(builder, decl); + declarationList.add(d); } + String declarations = BrailleCssSerializer.serializeDeclarationList(declarationList); + if (!declarations.isEmpty()) + builder.append(declarations).append(" "); builder.append("} "); } @@ -498,9 +535,10 @@ private static void insertHyphenationResourceDefinition(StringBuilder builder, R builder.append("@hyphenation-resource"); builder.append(":lang(").append(BrailleCssSerializer.serializeLanguageRanges(rule.getLanguageRanges())).append(")"); builder.append(" { "); - for (Declaration decl : rule) { - if (decl.size() == 1 && decl.get(0) instanceof TermURI) { - TermURI term = (TermURI)decl.get(0); + List declarationList = new ArrayList<>(); + for (Declaration d : rule) { + if (d.size() == 1 && d.get(0) instanceof TermURI) { + TermURI term = (TermURI)d.get(0); URI uri = URLs.asURI(term.getValue()); if (!uri.isAbsolute() && !uri.getSchemeSpecificPart().startsWith("/")) { // relative resource: make absolute and convert to "volatile-file" URI to bypass @@ -517,29 +555,53 @@ private static void insertHyphenationResourceDefinition(StringBuilder builder, R } catch (IllegalArgumentException e) { // not a file URI } + try { + d = createDeclaration(d.getProperty(), createTermURI(uri)); + } catch (RuntimeException e) { + e.printStackTrace(); + throw e; + } } - builder.append(decl.getProperty()).append(": ").append("url(\"" + uri + "\")").append("; "); - continue; } - insertDeclaration(builder, decl); + declarationList.add(d); } + String declarations = BrailleCssSerializer.serializeDeclarationList(declarationList); + if (!declarations.isEmpty()) + builder.append(declarations).append(" "); builder.append("} "); } + private static Declaration createDeclaration(String prop, Term val) { + return new DeclarationImpl() {{ + this.property = prop; + this.list = ImmutableList.of(val); + }}; + } + + private static TermURI createTermURI(URI uri) { + return new TermURIImpl() {{ + this.value = uri.toString(); + }}; + } + private static void insertCounterStyleDefinition(StringBuilder builder, RuleCounterStyle rule) { String name = rule.getName(); builder.append("@counter-style ").append(name).append(" { "); - for (Declaration decl : rule) - insertDeclaration(builder, decl); + String declarations = BrailleCssSerializer.serializeDeclarationList(rule); + if (!declarations.isEmpty()) + builder.append(declarations).append(" "); builder.append("} "); } - private static void insertAtRule(StringBuilder builder, AnyAtRule rule) { + private static void insertAtRule(StringBuilder builder, VendorAtRule> rule) { builder.append("@").append(rule.getName()).append(" { "); - for (Declaration decl : Iterables.filter(rule, Declaration.class)) - insertDeclaration(builder, decl); - for (AnyAtRule r : Iterables.filter(rule, AnyAtRule.class)) + String declarations = BrailleCssSerializer.serializeDeclarationList(Iterables.filter(rule, Declaration.class)); + if (!declarations.isEmpty()) + builder.append(declarations).append("; "); + for (VendorAtRule> r : Iterables.filter(rule, VendorAtRule.class)) insertAtRule(builder, r); + if (builder.substring(builder.length() - 2).equals("; ")) + builder.replace(builder.length() - 2, builder.length(), " "); builder.append("} "); } diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/BrailleCssParser.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/BrailleCssParser.java new file mode 100644 index 0000000000..547720075b --- /dev/null +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/BrailleCssParser.java @@ -0,0 +1,790 @@ +package org.daisy.pipeline.braille.css.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; + +import com.google.common.cache.CacheBuilder; + +import cz.vutbr.web.css.CSSProperty; +import cz.vutbr.web.css.CSSProperty.CounterIncrement; +import cz.vutbr.web.css.CSSProperty.CounterSet; +import cz.vutbr.web.css.CSSProperty.CounterReset; +import cz.vutbr.web.css.Declaration; +import cz.vutbr.web.css.NodeData; +import cz.vutbr.web.css.Term; +import cz.vutbr.web.css.TermList; + +import org.daisy.braille.css.BrailleCSSExtension; +import org.daisy.braille.css.BrailleCSSParserFactory; +import org.daisy.braille.css.BrailleCSSParserFactory.Context; +import org.daisy.braille.css.BrailleCSSProperty.Content; +import org.daisy.braille.css.BrailleCSSProperty.StringSet; +import org.daisy.braille.css.BrailleCSSProperty.TextTransform; +import org.daisy.braille.css.BrailleCSSRuleFactory; +import org.daisy.braille.css.PropertyValue; +import org.daisy.braille.css.SimpleInlineStyle; +import org.daisy.braille.css.SupportedBrailleCSS; +import org.daisy.common.spi.ServiceLoader; +import org.daisy.pipeline.braille.css.impl.ContentList.ContentFunction; +import org.daisy.pipeline.braille.css.TextStyleParser; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.w3c.dom.Element; + +public abstract class BrailleCssParser implements TextStyleParser { + + private static final Logger logger = LoggerFactory.getLogger(BrailleCssParser.class); + + private static BrailleCssParser INSTANCE_WITH_EXTENSIONS = null; + + public static BrailleCssParser getInstance() { + if (INSTANCE_WITH_EXTENSIONS == null) { + List extensions = new ArrayList<>(); + try { + Iterator i = getCSSExtensions(); + while (i.hasNext()) { + try { + BrailleCSSExtension extension = i.next(); + extensions.add(extension); + logger.debug("Binding BrailleCSSExtension: {}", extension); + } catch (Throwable e) { + logger.error("Error while binding BrailleCSSExtension", e); + } + } + } catch (Throwable e) { + logger.error("Error while binding BrailleCSSExtension services", e); + } + boolean allowUnknownVendorExtensions = false; + BrailleCSSParserFactory parserFactory = new BrailleCSSParserFactory( + new BrailleCSSRuleFactory(extensions, allowUnknownVendorExtensions)); + Map supportedBrailleCSS = new HashMap<>(); + INSTANCE_WITH_EXTENSIONS = new BrailleCssParser() {{ + for (Context context : new Context[]{Context.ELEMENT, Context.PAGE, Context.VOLUME}) + supportedBrailleCSS.put( + context, + new DeepDeclarationTransformer( + context, true, false, extensions, allowUnknownVendorExtensions)); + } + public BrailleCSSParserFactory getBrailleCSSParserFactory() { + return parserFactory; + } + public Optional getSupportedBrailleCSS(Context context) { + return Optional.ofNullable(supportedBrailleCSS.get(context)); + } + }; + } + return INSTANCE_WITH_EXTENSIONS; + } + + abstract BrailleCSSParserFactory getBrailleCSSParserFactory(); + + abstract Optional getSupportedBrailleCSS(Context context); + + private SupportedBrailleCSS supportedBrailleCSS = null; + + public final boolean isSupportedCSSProperty(String propertyName) { + if (supportedBrailleCSS == null) { + Optional o = getSupportedBrailleCSS(Context.ELEMENT); + if (!o.isPresent()) + throw new IllegalStateException(); + supportedBrailleCSS = o.get(); + } + return supportedBrailleCSS.isSupportedCSSProperty(propertyName); + } + + /** + * The returned object is immutable. + */ + public BrailleCssStyle parseInlineStyle(String style, Context context) { + if (context == null) + throw new IllegalArgumentException(); + BrailleCssStyle s = cache.get(context, style); + if (s == null) { + // try if a declaration was cached + if (context == Context.ELEMENT) { + Declaration d = declCache.get(style); + if (d != null) + s = BrailleCssStyle.of(d); + } + if (s == null) + s = BrailleCssStyle.of(this, context, style); + cache.put(context, style, s); + } + return s; + } + + /** + * Concretizes "inherit" even if the parent style is null or empty. + * + * The returned object is immutable. + */ + public BrailleCssStyle parseInlineStyle(String style, Context context, BrailleCssStyle parent) { + if (context != Context.ELEMENT) + throw new IllegalArgumentException(); + ParsedDeclarations parentDecls; { + if (parent == null || parent.declarations == null) + parentDecls = ParsedDeclarations.EMPTY; + else if (!(parent.declarations instanceof ParsedDeclarations)) + throw new IllegalArgumentException(); + else + parentDecls = (ParsedDeclarations)parent.declarations; + } + BrailleCssStyle s = cache.get(context, style, parentDecls, true); + if (s == null) { + s = parseInlineStyle(style, context); + s = s.inheritFrom(parentDecls); + cache.put(context, style, parentDecls, true, s); + } + return s; + } + + /** + * Style assumed to be specified in the context of a (pseudo-)element. + * + * @param context element for evaluating attr() and content() values against. + * @param mutable Whether the caller wishes to mutate the returned declaration. + */ + public Optional parseDeclaration(String property, String value, Element context, boolean mutable) { + String style = String.format("%s: %s", property, value); + Declaration declaration = declCache.get(style); + if (declaration == null) { + declaration = parseInlineStyle(style, Context.ELEMENT).getDeclaration(property); + if (declaration == null) + return Optional.empty(); + declCache.put(style, declaration); + } + if (context != null) { + Declaration evaluated = BrailleCssStyle.evaluateDeclaration(declaration, context); + if (evaluated != declaration) + return Optional.of(evaluated); + } + if (mutable) { + Declaration unlocked = BrailleCssStyle.unlockDeclaration(declaration); + if (unlocked != declaration) + return Optional.of(unlocked); + return Optional.of((Declaration)declaration.clone()); + } + return Optional.of(declaration); + } + + /** + * @param style assumed to be specified in the context of a (pseudo-)element + * @param context element for evaluating attr() and content() values against. + * @param mutable Whether the caller wishes to mutate the returned style object. + */ + public SimpleInlineStyle parseSimpleInlineStyle(String style, Element context, boolean mutable) { + BrailleCssStyle s = parseInlineStyle(style, Context.ELEMENT); + // evaluate attr() and content() values in content and string-set properties + if (context != null) + s = s.evaluate(context); + try { + return s.asSimpleInlineStyle(mutable); + } catch (UnsupportedOperationException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Concretizes "inherit" even if the parent style is null or empty. + */ + public SimpleInlineStyle parseSimpleInlineStyle(String style, + Element context, + SimpleInlineStyle parent, + boolean mutable) { + BrailleCssStyle s = parseInlineStyle( + style, Context.ELEMENT, parent != null ? BrailleCssStyle.of(parent) : null); + // evaluate attr() and content() values in content and string-set properties + if (context != null) + s = s.evaluate(context); + try { + return s.asSimpleInlineStyle(mutable); + } catch (UnsupportedOperationException e) { + throw new IllegalArgumentException(e); + } + } + + @Override // TextStyleParser + public SimpleInlineStyle parse(String style) { + if (style == null) style = ""; + // clone because we make SimpleInlineStyle available and SimpleInlineStyle is mutable (and we want it to be) + return parseSimpleInlineStyle(style, null, true); + } + + @Override // TextStyleParser + public SimpleInlineStyle parse(String style, SimpleInlineStyle parent) { + if (style == null) style = ""; + // clone because we make SimpleInlineStyle available and SimpleInlineStyle is mutable (and we want it to be) + return parseSimpleInlineStyle(style, null, parent, true); + } + + private final Map declCache = CacheBuilder.newBuilder() + .expireAfterAccess(60, TimeUnit.SECONDS) + .build() + .asMap(); + + /* =================================================================== */ + /* The fields and classes below are used only from BrailleCssStyle but */ + /* we keep it in this class because it is all related with parsing. */ + /* =================================================================== */ + + /** + * {@link SimpleInlineStyle} that is part of a {@link BrailleCssStyle} and created + * by {@link BrailleCssParser}. + * + * The purpose of having this class instead of {@link SimpleInlineStyle} is: + * + * 1. to easier recognize styles that were created by {@link BrailleCssParser}. + * + * 2. to control by whom and when styles can be mutated. Objects are mutable, but should be regarded as + * being immutable outside this class, unless the {@code locked} field was set to {@code false}. + */ + static class ParsedDeclarations extends SimpleInlineStyle { + + public static final ParsedDeclarations EMPTY = new ParsedDeclarations(); + private BrailleCssParser parser; + private Context context; + + /** + * @param parser {@link BrailleCssParser} to be used to process declarations that are not {@link + * ParsedDeclaration} instances. If non-{@code null}, assert that declarations that are + * {@link ParsedDeclaration} were created using the given {@link BrailleCssParser}. If + * {@code null}, assert that all {@link ParsedDeclaration} declarations were created with + * the same {@link BrailleCssParser}. + */ + ParsedDeclarations(BrailleCssParser parser, Context context, Iterable declarations) { + super(declarations, null, parser != null ? parser.getSupportedBrailleCSS(context).get() : null); + this.parser = parser; + this.context = context; + checkParserAndContext(); + } + + // for ParsedDeclarations.EMPTY + private ParsedDeclarations() { + super(null, null, null); + parser = null; + context = null; + } + + /** + * Check that all declarations were parsed with the same parser and context, + * and attach parser and context to the Quadruple. + */ + private void checkParserAndContext() { + for (Map.Entry e : map.entrySet()) { + ParsedDeclaration d = new ParsedDeclaration(parser, context, super.get(e.getKey())); + if (parser == null) + parser = d.getParser(); + if (context == null) + context = d.getContext(); + e.setValue(d.quadruple); + } + } + + // This is a bit of a lazy implementation of the object's immutability, because it is never + // actually enforced that the object is not mutated when it is "locked". It is difficult to + // actually enforce it because a SimpleInlineStyle can be mutated in various ways: + // declarations can be removed or values can be mutated. This approach is however safe + // enough, because whenever the object is exposed to the outside (namely through the + // TextStyleParser/CSSStyledText API), a copy is made. When exposed through the XPath API, no + // copying is needed because the XPath API doesn't allow for mutation. + + // object should always be cloned before it is unlocked + boolean locked = true; + + BrailleCssParser getParser() { + return parser; + } + + Context getContext() { + return context; + } + + // override only method of SimpleInlineStyle that creates new instances of PropertyValue + @Override + public PropertyValue get(String property) { + PropertyValue d = super.get(property); + if (d != null) + d = new ParsedDeclaration(parser, context, d); + return d; + } + + public PropertyValue getOrDefault(String property) { + PropertyValue d = get(property); + if (d == null && parser != null) { + SupportedBrailleCSS css = parser.getSupportedBrailleCSS(context).orElse(null); + if (css != null) + return new ParsedDeclaration( + parser, + context, + property, + css.getDefaultProperty(property), + css.getDefaultValue(property), + null); + } + return d; + } + + @Override + public ParsedDeclarations inheritFrom(NodeData parent) { + if (!(parent instanceof ParsedDeclarations)) + throw new IllegalArgumentException(); + ParsedDeclarations r = (ParsedDeclarations)super.inheritFrom(parent); + BrailleCssParser p = ((ParsedDeclarations)parent).parser; + if (r.parser == null) + r.parser = p; + else if (p != null && r.parser != p) + throw new IllegalArgumentException(); + Context c = ((ParsedDeclarations)parent).context; + if (r.context == null) + r.context = c; + else if (c != null && r.context != c) + throw new IllegalArgumentException(); + r.checkParserAndContext(); + if (!r.isEmpty()) { + // Make sure that the resulting SimpleInlineStyle is not based on a parent and that it + // is not concretized because that would result in an exception when inheritFrom() is + // called on it. Note that concretize() does not have the same effect. + // Also perform special inheritance of text-transform. + List list = new ArrayList<>(); + r.forEach(list::add); + ListIterator i = list.listIterator(); + while (i.hasNext()) { + ParsedDeclaration v = (ParsedDeclaration)i.next(); + PropertyValue flat = null; + if (!((ParsedDeclarations)parent).isEmpty() && v.getCSSProperty() instanceof TextTransform) + for (PropertyValue vv : (ParsedDeclarations)parent) + if (vv.getCSSProperty() instanceof TextTransform) { + TextTransformList t = TextTransformList.of(v); + t.inheritFrom(TextTransformList.of(vv)); // this mutates the value + if (t != v.getValue()) + flat = new ParsedDeclaration( + v.getParser(), + v.getContext(), + v.getProperty(), + t.equalsAuto() ? TextTransform.AUTO + : t.equalsInitial() ? TextTransform.INITIAL + : t.equalsNone() ? TextTransform.NONE + : TextTransform.list_values, + t.equalsAuto() || t.equalsInitial() || t.equalsNone() ? null : t, + v.getSourceDeclaration()); + break; + } + if (flat == null) + flat = new ParsedDeclaration( + v.getParser(), + v.getContext(), + v.getProperty(), + v.getCSSProperty(), + v.getValue(), + v.getSourceDeclaration()); + i.set(flat); + } + r = new ParsedDeclarations(r.parser, r.context, list); + } + return r; + } + } + + /** + * Declaration with reference to used parser and parser context. May cache + * itself when it is serialized. + * + * Clones are never cached. + */ + static class ParsedDeclaration extends PropertyValue { + + private final BrailleCssParser parser; + private final Context context; + private final Quadruple quadruple; // to make propertyValue available outside this class + private boolean enableCaching = false; + + /** + * @throws IllegalArgumentException if the used parser and parser context can + * not be derived from the declaration. + */ + ParsedDeclaration(PropertyValue declaration) { + this(null, null, declaration); + } + + /** + * @param parser If {@code null}, parser is derived from declaration + * @param context If {@code null}, context is derived from declaration + */ + ParsedDeclaration(BrailleCssParser parser, Context context, PropertyValue declaration) { + super(declaration); + if (this.propertyValue instanceof Quadruple) { + this.parser = ((Quadruple)this.propertyValue).getParser(); + this.context = ((Quadruple)this.propertyValue).getContext(); + if (parser != null && this.parser != parser) + throw new IllegalArgumentException(); + if (context != null && this.context != context) + throw new IllegalArgumentException(); + } else { + if (parser == null || context == null) + throw new IllegalArgumentException(); + this.parser = parser; + this.context = context; + this.propertyValue = new Quadruple(this.getProperty(), + this.getCSSProperty(), + this.getValue(), + this.getSourceDeclaration(), + this.getSupportedBrailleCSS()); + } + this.quadruple = (Quadruple)this.propertyValue; + this.enableCaching = (this.context == Context.ELEMENT); + } + + ParsedDeclaration(BrailleCssParser parser, + Context context, + String propertyName, + CSSProperty property, + Term value, + Declaration sourceDeclaration) { + this(parser, context, new PropertyValue(propertyName, + property, + value, + sourceDeclaration, + parser.getSupportedBrailleCSS(context).get())); + } + + BrailleCssParser getParser() { + return parser; + } + + Context getContext() { + return context; + } + + @Override + public ParsedDeclaration getDefault() { + PropertyValue d = super.getDefault(); + if (d == null) + return null; + else if (d instanceof ParsedDeclaration) + return (ParsedDeclaration)d; + else + return new ParsedDeclaration(parser, context, d); + } + + private String valueSerialized = null; + private String serialized = null; + + @Override + public String toString() { + if (serialized == null) + valueToString(); // this also sets serialized and updates cache + return serialized; + } + + // for use in BrailleCssSerializer.serializePropertyValue() + String valueToString() { + if (valueSerialized == null) { + valueSerialized = BrailleCssSerializer.serializePropertyValue(new PropertyValue(this)); + serialized = getProperty() + ": " + valueSerialized; + if (enableCaching) + parser.declCache.put(serialized, this); + } else { + // access cache to keep entry longer in it + if (enableCaching) + parser.declCache.get(serialized); + } + return valueSerialized; + } + + @Override + public ParsedDeclaration clone() { + ParsedDeclaration clone = (ParsedDeclaration)super.clone(); + // Clones are never cached because we assume that clones are made to expose a + // declaration to the outside, where they may possibly be mutated. If a clone is made + // for another purpose, such as for running evaluate() on it, caching might be + // desirable. Luckily, evalutation is normally already done before a declaration is + // serialized. An alternative would be to cache copies of the object every time + // toString() is called, and to also make copies when objects are retreived from the + // cache. This would allow objects to be mutated. + clone.enableCaching = false; + // forget serialization because object may be mutated after it has been cloned + clone.valueSerialized = null; + clone.serialized = null; + return clone; + } + + private class Quadruple extends cz.vutbr.web.domassign.SingleMapNodeData.Quadruple { + + private Quadruple(String propertyName, + final CSSProperty property, + final Term value, + final Declaration sourceDeclaration, + SupportedBrailleCSS css) { + super(css, propertyName); + curProp = property; + curValue = value; + curSource = sourceDeclaration; + } + + private BrailleCssParser getParser() { + return ParsedDeclaration.this.getParser(); + } + + private Context getContext() { + return ParsedDeclaration.this.getContext(); + } + } + } + + // used in BrailleCssSerializer + final Cache cache = new Cache(); + + class Cache { + + private final Map cache = CacheBuilder.newBuilder() + .expireAfterAccess(60, TimeUnit.SECONDS) + .build() + .asMap(); + void put(Context context, String serializedStyle, BrailleCssStyle style) { + put(context, serializedStyle, null, false, style); + } + + void put(Context context, String serializedStyle, ParsedDeclarations parent, boolean concretizeInherit, BrailleCssStyle style) { + if (context == null) return; + CacheKey key = new CacheKey(context, serializedStyle, parent, concretizeInherit); + cache.put(key, style); + } + + BrailleCssStyle get(Context context, String serializedStyle) { + return get(context, serializedStyle, null, false); + } + + BrailleCssStyle get(Context context, String serializedStyle, ParsedDeclarations parent, boolean concretizeInherit) { + if (context == null) return null; + CacheKey key = new CacheKey(context, serializedStyle, parent, concretizeInherit); + return cache.get(key); + } + } + + private static class CacheKey implements Comparable { + private final Context context; + private final String style; + private final String parent; + private final boolean concretizeInherit; + public CacheKey(Context context, String style, ParsedDeclarations parent, boolean concretizeInherit) { + if (parent != null && context != Context.ELEMENT) + throw new IllegalArgumentException(); + this.context = context; + this.style = style; + this.parent = parent != null ? BrailleCssSerializer.toString(parent) : null; + this.concretizeInherit = concretizeInherit; + } + @Override + public int compareTo(CacheKey that) { + int i = this.context.compareTo(that.context); + if (i != 0) + return i; + i = this.style.compareTo(that.style); + if (i != 0) + return i; + if (this.parent == null) + return that.parent == null ? 0 : -1; + else if (that.parent == null) + return 1; + i = this.parent.compareTo(that.parent); + if (i != 0) + return i; + if (this.concretizeInherit == that.concretizeInherit) + return 0; + else + return this.concretizeInherit ? -1 : 1; + } + @Override + public int hashCode() { + final int prime = 31; + int hash = 1; + hash = prime * hash + context.hashCode(); + hash = prime * hash + style.hashCode(); + hash = prime * hash + (parent == null ? 0 : parent.hashCode()); + hash = prime * hash + Boolean.hashCode(concretizeInherit); + return hash; + } + @Override + public boolean equals(Object o) { + if (o == null) + return false; + if (!(o instanceof CacheKey)) + return false; + CacheKey that = (CacheKey)o; + if (this.context != that.context) + return false; + if (!this.style.equals(that.style)) + return false; + if (this.parent == null) + return that.parent == null; + else if (that.parent == null) + return false; + else if (!this.parent.equals(that.parent)) + return false; + else + return this.concretizeInherit == that.concretizeInherit; + } + } + + private class DeepDeclarationTransformer extends SupportedBrailleCSS { + + private final Context context; + + public DeepDeclarationTransformer(Context context, + boolean allowComponentProperties, + boolean allowShorthandProperties, + Collection extensions, + boolean allowUnknownVendorExtensions) { + super(allowComponentProperties, allowShorthandProperties, extensions, allowUnknownVendorExtensions); + this.context = context; + } + + @Override + public boolean parseDeclaration(Declaration d, Map properties, Map> values) { + if ("text-transform".equalsIgnoreCase(d.getProperty())) { + if (super.processTextTransform(d, properties, values)) { + if (properties.get("text-transform") == TextTransform.list_values) { + Term value = values.get("text-transform"); + if (value instanceof TermList) + values.put("text-transform", TextTransformList.of((TermList)value)); + else + throw new IllegalStateException(); // should not happen + } + return true; + } else + return false; + } else if ("content".equalsIgnoreCase(d.getProperty())) { + if (super.processContent(d, properties, values)) { + if (properties.get("content") == Content.content_list) { + Term value = values.get("content"); + if (value instanceof TermList) { + ContentList l = ContentList.of(BrailleCssParser.this, extensions, context, (TermList)value); + for (Term t : l) + if (t instanceof ContentFunction && !((ContentFunction)t).target.isPresent()) + throw new IllegalArgumentException("unexpected term in content list: " + t); + values.put("content", l); + } else + throw new IllegalStateException(); // should not happen + } + return true; + } else + return false; + } else if ("string-set".equalsIgnoreCase(d.getProperty())) { + if (super.processStringSet(d, properties, values)) { + if (properties.get("string-set") == StringSet.list_values) { + Term value = values.get("string-set"); + if (value instanceof TermList) + values.put("string-set", StringSetList.of(BrailleCssParser.this, context, (TermList)value)); + else + throw new IllegalStateException(); // should not happen + } + return true; + } else + return false; + } else if ("counter-set".equalsIgnoreCase(d.getProperty())) { + if (super.processCounterSet(d, properties, values)) { + if (properties.get("counter-set") == CounterSet.list_values) { + Term value = values.get("counter-set"); + if (value instanceof TermList) + values.put("counter-set", CounterSetList.of((TermList)value)); + else + throw new IllegalStateException(); // should not happen + } + return true; + } else + return false; + } else if ("counter-reset".equalsIgnoreCase(d.getProperty())) { + if (super.processCounterReset(d, properties, values)) { + if (properties.get("counter-reset") == CounterReset.list_values) { + Term value = values.get("counter-reset"); + if (value instanceof TermList) + values.put("counter-reset", CounterSetList.of((TermList)value)); + else + throw new IllegalStateException(); // should not happen + } + return true; + } else + return false; + } else if ("counter-increment".equalsIgnoreCase(d.getProperty())) { + if (super.processCounterIncrement(d, properties, values)) { + if (properties.get("counter-increment") == CounterIncrement.list_values) { + Term value = values.get("counter-increment"); + if (value instanceof TermList) + values.put("counter-increment", CounterSetList.of((TermList)value)); + else + throw new IllegalStateException(); // should not happen + } + return true; + } else + return false; + } else + return super.parseDeclaration(d, properties, values); + } + } + + private static Iterator getCSSExtensions() { + if (OSGiHelper.inOSGiContext()) + return OSGiHelper.getCSSExtensions(); + else + return SPIHelper.getCSSExtensions(); + } + + // static nested class in order to delay class loading + private static abstract class OSGiHelper { + + static boolean inOSGiContext() { + try { + return FrameworkUtil.getBundle(OSGiHelper.class) != null; + } catch (NoClassDefFoundError e) { + return false; + } + } + + static Iterator getCSSExtensions() { + return new Iterator() { + private BundleContext bc = FrameworkUtil.getBundle(BrailleCssParser.class).getBundleContext(); + private ServiceReference[] refs; + private int index = 0; { + try { + refs = bc.getServiceReferences(BrailleCSSExtension.class.getName(), null); + } catch (InvalidSyntaxException e) { + throw new IllegalStateException(e); // should not happen + } + } + public boolean hasNext() { + return refs != null && index < refs.length; + } + public BrailleCSSExtension next() throws NoSuchElementException { + if (!hasNext()) + throw new NoSuchElementException(); + return (BrailleCSSExtension)bc.getService(refs[index++]); + } + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + } + + // static nested class in order to delay class loading + private static abstract class SPIHelper { + static Iterator getCSSExtensions() { + return ServiceLoader.load(BrailleCSSExtension.class).iterator(); + } + } +} diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/BrailleCssSerializer.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/BrailleCssSerializer.java index dc771e7f30..c791f4b925 100644 --- a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/BrailleCssSerializer.java +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/BrailleCssSerializer.java @@ -1,6 +1,8 @@ package org.daisy.pipeline.braille.css.impl; +import java.net.URI; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -11,25 +13,48 @@ import javax.xml.stream.XMLStreamWriter; import com.google.common.collect.Iterables; +import com.google.common.collect.Ordering; +import cz.vutbr.web.css.CSSProperty; +import cz.vutbr.web.css.CSSProperty.CounterIncrement; +import cz.vutbr.web.css.CSSProperty.CounterSet; +import cz.vutbr.web.css.CSSProperty.CounterReset; import cz.vutbr.web.css.Declaration; +import cz.vutbr.web.css.NodeData; import cz.vutbr.web.css.Rule; import cz.vutbr.web.css.RuleBlock; import cz.vutbr.web.css.RulePage; import cz.vutbr.web.css.Selector; import cz.vutbr.web.css.Selector.Combinator; import cz.vutbr.web.css.Term; +import cz.vutbr.web.css.TermFunction; +import cz.vutbr.web.css.TermString; +import cz.vutbr.web.css.TermURI; import cz.vutbr.web.csskit.OutputUtil; import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttribute; import static org.daisy.common.stax.XMLStreamWriterHelper.writeStartElement; +import org.daisy.braille.css.BrailleCSSParserFactory.Context; +import org.daisy.braille.css.BrailleCSSProperty.Content; +import org.daisy.braille.css.BrailleCSSProperty.StringSet; +import org.daisy.common.file.URLs; +import org.daisy.pipeline.braille.css.impl.BrailleCssParser.ParsedDeclaration; +import org.daisy.pipeline.braille.css.impl.BrailleCssParser.ParsedDeclarations; +import org.daisy.pipeline.braille.css.impl.ContentList.AttrFunction; +import org.daisy.pipeline.braille.css.impl.ContentList.ContentFunction; +import org.daisy.pipeline.braille.css.impl.ContentList.CounterFunction; +import org.daisy.pipeline.braille.css.impl.ContentList.CounterStyle; +import org.daisy.pipeline.braille.css.impl.ContentList.FlowFunction; +import org.daisy.pipeline.braille.css.impl.ContentList.LeaderFunction; +import org.daisy.pipeline.braille.css.impl.ContentList.StringFunction; +import org.daisy.pipeline.braille.css.impl.ContentList.TextFunction; +import org.daisy.pipeline.braille.css.impl.ContentList.URL; import org.daisy.braille.css.InlineStyle; import org.daisy.braille.css.InlineStyle.RuleMainBlock; import org.daisy.braille.css.InlineStyle.RuleRelativeBlock; import org.daisy.braille.css.LanguageRange; import org.daisy.braille.css.PropertyValue; -import org.daisy.braille.css.SimpleInlineStyle; import org.daisy.pipeline.css.CssSerializer; public final class BrailleCssSerializer { @@ -41,51 +66,166 @@ private BrailleCssSerializer() {} /* =================================================== */ public static String toString(Term term) { - return CssSerializer.toString(term); + if (term instanceof TextTransformList || + term instanceof ContentList || + term instanceof StringSetList || + term instanceof CounterSetList) + return serializeTermList((List>)term); + else if (term instanceof AttrFunction) { + AttrFunction f = (AttrFunction)term; + String s = "attr(" + toString(f.name); + if (f.asURL) + s = s + " url"; + s += ")"; + return s; } + else if (term instanceof ContentFunction) { + ContentFunction f = (ContentFunction)term; + if (f.target.isPresent()) + return "target-content(" + toString(f.target.get()) + ")"; + else + return "content()"; } + else if (term instanceof StringFunction) { + StringFunction f = (StringFunction)term; + if (f.target.isPresent()) + return "target-string(" + toString(f.target.get()) + ", " + toString(f.name) + ")"; + else { + String s = "string(" + toString(f.name); + if (f.scope.isPresent()) + s = s + ", " + f.scope.get().toString(); + s += ")"; + return s; }} + else if (term instanceof CounterFunction) { + CounterFunction f = (CounterFunction)term; + String s = ""; + if (f.target.isPresent()) + s = "target-counter(" + toString(f.target.get()) + ", " + toString(f.name); + else + s = "counter(" + toString(f.name); + if (f.style.isPresent()) + s = s + ", " + toString(f.style.get()); + s += ")"; + return s; } + else if (term instanceof TextFunction) { + TextFunction f = (TextFunction)term; + return "target-text(" + toString(f.target) + ")"; } + else if (term instanceof LeaderFunction) { + LeaderFunction f = (LeaderFunction)term; + String s = "leader(" + toString(f.pattern); + if (f.position.isPresent()) + s = s + ", " + toString(f.position.get()); + if (f.alignment.isPresent()) + s = s + ", " + f.alignment.get().toString(); + s += ")"; + return s; } + else if (term instanceof FlowFunction) { + FlowFunction f = (FlowFunction)term; + String s = "flow(" + toString(f.from); + if (f.scope.isPresent()) + s = s + ", " + f.scope.get().toString(); + s += ")"; + return s; } + else + return CssSerializer.toString(term, t -> toString(t)); } public static String toString(Declaration declaration) { - return declaration.getProperty() + ": " + serializeTermList((List>)declaration) + ";"; + if (declaration instanceof PropertyValue) + return declaration.getProperty() + ": " + serializePropertyValue((PropertyValue)declaration); + else + return declaration.getProperty() + ": " + serializeTermList(declaration); + } + + public static String toString(BrailleCssStyle style) { + if (style.serialized == null) { + style.serialized = toString(style, null, null); + // cache + if (style.parser != null) + style.parser.cache.put(style.context, style.serialized, style); + } else { + // access cache to keep entry longer in it + if (style.parser != null) + style.parser.cache.get(style.context, style.serialized); + } + return style.serialized; } - public static String toString(BrailleCssTreeBuilder.Style style) { - return toString(style, null); + /** + * @param relativeTo If not {@code null}, include only those declarations that are needed + * to reconstruct {@code style} with {@code relativeTo} as the parent + * style. Relativizes even if the parent style is empty. + */ + public static String toString(BrailleCssStyle style, BrailleCssStyle relativeTo) { + if (relativeTo == null) + return toString(style); + String s = toString(style.relativize(relativeTo)); + // cache + if (style.parser != null) + style.parser.cache.put(style.context, + s, + relativeTo.declarations != null + ? (ParsedDeclarations)relativeTo.declarations + : ParsedDeclarations.EMPTY, + true, + style); + return s; } - private static String toString(BrailleCssTreeBuilder.Style style, String base) { + public static String toString(BrailleCssStyle style, String indentation) { + // this function does update the cache, nor does it store the string in the style object, as + // it's meant to pretty print a style (for the purpose of showing in temporary files or log + //messages) + return toString(style, null, indentation); + } + + private static String toString(BrailleCssStyle style, String base, String indent) { StringBuilder b = new StringBuilder(); StringBuilder rel = new StringBuilder(); - if (style.declarations != null) - b.append(serializeDeclarationList(style.declarations)); + if ("".equals(indent)) indent = null; + String newline = indent != null ? "\n" : " "; + if (style.declarations != null) { + b.append(serializeDeclarationList(style.declarations, ";" + newline)); + if (indent != null && b.length() > 0) b.append(";"); + } if (style.nestedStyles != null) - for (Map.Entry e : style.nestedStyles.entrySet()) { + for (Map.Entry e : style.nestedStyles.entrySet()) { if (base != null && e.getKey().startsWith("&")) { - if (rel.length() > 0) rel.append(" "); - rel.append(toString(e.getValue(), base + e.getKey().substring(1))); + if (rel.length() > 0) rel.append(newline); + rel.append(toString(e.getValue(), base + e.getKey().substring(1), indent)); } else { - if (b.length() > 0) b.append(" "); - b.append(toString(e.getValue(), e.getKey())); + if (b.length() > 0) { + if (indent == null && b.charAt(b.length() - 1) != '}') b.append(";"); + b.append(newline); + } + b.append(toString(e.getValue(), e.getKey(), indent)); } } if (base != null && b.length() > 0) { - b.insert(0, base + " { "); - b.append(" }"); + if (indent != null) { + String s = b.toString(); + s = indent + s.trim().replaceAll("\n", "\n" + indent); + b.setLength(0); + b.append(s); + } + b.insert(0, base + " {" + newline); + b.append(newline + "}"); } if (rel.length() > 0) { - if (b.length() > 0) b.append(" "); + if (b.length() > 0) { + if (indent == null && b.charAt(b.length() - 1) != '}') b.append(";"); + b.append(newline); + } b.append(rel); } return b.toString(); } - public static String toString(InlineStyle style) { - return toString(BrailleCssTreeBuilder.Style.of(style)); - } - - public static String toString(SimpleInlineStyle style) { + public static String toString(NodeData style) { List declarations = new ArrayList<>(); - for (String p : style.getPropertyNames()) - declarations.add(p + ": " + serializePropertyValue(style.get(p))); + for (String p : style.getPropertyNames()) { + String v = serializePropertyValue(style, p); + if (v != null) + declarations.add(p + ": " + v); + } Collections.sort(declarations); StringBuilder s = new StringBuilder(); Iterator it = declarations.iterator(); @@ -96,12 +236,30 @@ public static String toString(SimpleInlineStyle style) { return s.toString(); } - public static String serializePropertyValue(PropertyValue propValue) { - Term value = propValue.getValue(); + public static String serializePropertyValue(NodeData style, String property) { + return serializePropertyValue(style, property, true); + } + + public static String serializePropertyValue(NodeData style, String property, boolean includeInherited) { + Term value = style.getValue(property, includeInherited); if (value != null) return toString(value); - else - return propValue.getProperty().toString(); + else { + CSSProperty p = style.getProperty(property, includeInherited); + return p != null ? p.toString() : null; + } + } + + public static String serializePropertyValue(PropertyValue propValue) { + if (propValue instanceof ParsedDeclaration) + return ((ParsedDeclaration)propValue).valueToString(); // this may trigger caching + else { + Term value = propValue.getValue(); + if (value != null) + return toString(value); + else + return propValue.getCSSProperty().toString(); + } } public static String toString(RuleMainBlock rule) { @@ -141,11 +299,8 @@ else if (r instanceof RulePage) return b.toString(); } - public static String toString(RulePage page) { - return toString( - new BrailleCssTreeBuilder.Style().add( - "@page", - BrailleCssTreeBuilder.Style.of(page))); + public static String toString(RulePage page, BrailleCssParser parser) { + return toString(BrailleCssStyle.of(parser, page)); } public static String serializeRuleBlockList(Iterable>> ruleBlocks) { @@ -170,74 +325,315 @@ else if (r instanceof RuleRelativeBlock) return b; } - public static String serializeTermList(List> termList) { - return CssSerializer.serializeTermList(termList); + public static String serializeTermList(Collection> termList) { + return CssSerializer.serializeTermList(termList, t -> toString(t)); } public static String serializeLanguageRanges(List languageRanges) { return OutputUtil.appendList(new StringBuilder(), languageRanges, OutputUtil.SELECTOR_DELIM).toString(); } - /* = PRIVATE ========================================= */ + public static String serializeDeclarationList(Iterable declarations) { + return serializeDeclarationList(declarations, "; "); + } - private static String serializeDeclarationList(Iterable declarations) { - List sortedDeclarations = new ArrayList(); - for (Declaration d : declarations) sortedDeclarations.add(d); + private static String serializeDeclarationList(Iterable declarations, String separator) { + List sortedDeclarations = new ArrayList<>(); + for (Declaration d : declarations) + sortedDeclarations.add(BrailleCssSerializer.toString(d)); Collections.sort(sortedDeclarations); StringBuilder s = new StringBuilder(); - Iterator it = sortedDeclarations.iterator(); + Iterator it = sortedDeclarations.iterator(); while (it.hasNext()) { - s.append(BrailleCssSerializer.toString(it.next())); - if (it.hasNext()) s.append(" "); + s.append(it.next()); + if (it.hasNext()) s.append(separator); } return s.toString(); } + /* = PRIVATE ========================================= */ + + private static String toString(URL url) { + if (url.url != null) + return toString(url.url); + else + return toString(url.urlAttr); + } + + private static String toString(CounterStyle style) { + if (style.name != null) + return style.name; + else if (style.symbol != null) + return toString(style.symbol); + else if (style.symbols != null) + return toString(style.symbols); + else + return "none"; + } + + /* =================================================== */ + /* toAttributes */ + /* =================================================== */ + + public static void toAttributes(BrailleCssStyle style, XMLStreamWriter writer) throws XMLStreamException { + if (style.nestedStyles != null) + throw new UnsupportedOperationException(); + if (style.declarations != null) + for (Declaration d : style.declarations) + toAttribute(d, writer); + } + + /** + * @param relativeTo If not {@code null}, include only those declarations that are needed + * to reconstruct {@code style} with {@code relativeTo} as the parent + * style. Relativizes even if the parent style is empty. + */ + public static void toAttributes(BrailleCssStyle style, BrailleCssStyle relativeTo, XMLStreamWriter writer) + throws XMLStreamException { + if (relativeTo == null) + toAttributes(style, writer); + else + toAttributes(style.relativize(relativeTo), writer); + } + + private static void toAttribute(Declaration declaration, XMLStreamWriter writer) throws XMLStreamException { + writeAttribute(writer, + new QName(XMLNS_CSS, declaration.getProperty().replaceAll("^-", "_"), "css"), + declaration instanceof PropertyValue + ? serializePropertyValue((PropertyValue)declaration) + : serializeTermList(declaration)); + } + /* =================================================== */ /* toXml */ /* =================================================== */ - private static final String XMLNS_CSS = "http://www.daisy.org/ns/pipeline/braille-css"; - private static final QName CSS_RULE = new QName(XMLNS_CSS, "rule", "css"); - private static final QName CSS_PROPERTY = new QName(XMLNS_CSS, "property", "css"); - private static final QName SELECTOR = new QName("selector"); - private static final QName STYLE = new QName("style"); - private static final QName NAME = new QName("name"); - private static final QName VALUE = new QName("value"); + private static final String XMLNS_CSS = "http://www.daisy.org/ns/pipeline/braille-css"; + private static final QName CSS_RULE = new QName(XMLNS_CSS, "rule", "css"); + private static final QName CSS_PROPERTY = new QName(XMLNS_CSS, "property", "css"); + private static final QName CSS_STRING = new QName(XMLNS_CSS, "string", "css"); + private static final QName CSS_CONTENT = new QName(XMLNS_CSS, "content", "css"); + private static final QName CSS_ATTR = new QName(XMLNS_CSS, "attr", "css"); + private static final QName CSS_COUNTER = new QName(XMLNS_CSS, "counter", "css"); + private static final QName CSS_TEXT = new QName(XMLNS_CSS, "text", "css"); + private static final QName CSS_LEADER = new QName(XMLNS_CSS, "leader", "css"); + private static final QName CSS_FLOW = new QName(XMLNS_CSS, "flow", "css"); + private static final QName CSS_CUSTOM_FUNC = new QName(XMLNS_CSS, "custom-func", "css"); + private static final QName CSS_STRING_SET = new QName(XMLNS_CSS, "string-set", "css"); + private static final QName CSS_COUNTER_SET = new QName(XMLNS_CSS, "counter-set", "css"); + private static final QName SELECTOR = new QName("selector"); + private static final QName NAME = new QName("name"); + private static final QName VALUE = new QName("value"); + private static final QName STYLE = new QName("style"); + private static final QName TARGET = new QName("target"); + private static final QName TARGET_ATTRIBUTE = new QName("target-attribute"); + private static final QName SCOPE = new QName("scope"); + private static final QName PATTERN = new QName("pattern"); + private static final QName POSITION = new QName("position"); + private static final QName ALIGNMENT = new QName("alignment"); + private static final QName FROM = new QName("from"); - public static void toXml(BrailleCssTreeBuilder.Style style, XMLStreamWriter writer, boolean deep) throws XMLStreamException { - toXml(style, writer, deep, false); + public static void toXml(BrailleCssStyle style, XMLStreamWriter writer) throws XMLStreamException { + toXml(style, writer, false); } - private static void toXml(BrailleCssTreeBuilder.Style style, + private static void toXml(BrailleCssStyle style, XMLStreamWriter w, - boolean deep, boolean recursive) throws XMLStreamException { - if (style.declarations != null) { - if (!deep || !recursive || style.nestedStyles != null) + if (style.declarations != null && !Iterables.isEmpty(style.declarations)) { + if (!recursive || style.nestedStyles != null) writeStartElement(w, CSS_RULE); - if (!deep) - writeAttribute(w, STYLE, serializeDeclarationList(style.declarations)); - if (deep) { - for (Declaration d : style.declarations) { - writeStartElement(w, CSS_PROPERTY); - writeAttribute(w, NAME, d.getProperty()); - writeAttribute(w, VALUE, serializeTermList((List>)d)); - w.writeEndElement(); - } - } - if (!deep || !recursive || style.nestedStyles != null) + List declarations = new ArrayList<>(); + for (Declaration d : style.declarations) declarations.add(d); + Collections.sort(declarations, Ordering.natural().onResultOf(Declaration::getProperty)); + for (Declaration d : declarations) + toXml(d, w); + if (!recursive || style.nestedStyles != null) w.writeEndElement(); } if (style.nestedStyles != null) - for (Map.Entry e : style.nestedStyles.entrySet()) { + for (Map.Entry e : style.nestedStyles.entrySet()) { writeStartElement(w, CSS_RULE); writeAttribute(w, SELECTOR, e.getKey()); - if (!deep) - writeAttribute(w, STYLE, e.getValue().toString()); - if (deep) - toXml(e.getValue(), w, true, true); + toXml(e.getValue(), w, true); w.writeEndElement(); } } + + public static void toXml(Declaration declaration, XMLStreamWriter writer) throws XMLStreamException { + if (declaration instanceof PropertyValue) + toXml((PropertyValue)declaration, writer); + else { + writeStartElement(writer, CSS_PROPERTY); + writeAttribute(writer, NAME, declaration.getProperty()); + writeAttribute(writer, VALUE, serializeTermList(declaration)); + writer.writeEndElement(); + } + } + + public static void toXml(PropertyValue value, XMLStreamWriter writer) throws XMLStreamException { + writeStartElement(writer, CSS_PROPERTY); + writeAttribute(writer, NAME, value.getProperty()); + CSSProperty p = value.getCSSProperty(); + if (p == Content.NONE || p == Content.INITIAL) + ; + else if (p == Content.INHERIT) + writeAttribute(writer, VALUE, Content.INHERIT.toString()); + else if (p == Content.content_list) { + if (value.getValue() instanceof ContentList) + toXml((ContentList)value.getValue(), writer); + else + throw new IllegalArgumentException(); + } else if (p == StringSet.NONE || p == StringSet.INITIAL) + ; + else if (p == StringSet.INHERIT) + writeAttribute(writer, VALUE, StringSet.INHERIT.toString()); + else if (p == StringSet.list_values) + if (value.getValue() instanceof StringSetList) + toXml((StringSetList)value.getValue(), writer); + else + throw new IllegalArgumentException(); + else if (p == CounterSet.NONE || p == CounterSet.INITIAL || + p == CounterReset.NONE || p == CounterReset.INITIAL || + p == CounterIncrement.NONE || p == CounterIncrement.INITIAL) + ; + else if (p == CounterSet.INHERIT || + p == CounterReset.INHERIT || + p == CounterIncrement.INHERIT) + writeAttribute(writer, VALUE, "inherit"); + else if (p == CounterSet.list_values || + p == CounterReset.list_values || + p == CounterIncrement.list_values) + if (value.getValue() instanceof CounterSetList) + toXml((CounterSetList)value.getValue(), writer); + else + throw new IllegalArgumentException(); + else + writeAttribute(writer, VALUE, serializePropertyValue(value)); + writer.writeEndElement(); + } + + public static void toXml(ContentList list, XMLStreamWriter w) throws XMLStreamException { + for (Term i : list) + if (i instanceof TermString || i instanceof TermURI) { + Term s = (Term)i; + writeStartElement(w, CSS_STRING); + writeAttribute(w, VALUE, s.getValue()); + w.writeEndElement(); } + else if (i instanceof AttrFunction) { + AttrFunction f = (AttrFunction)i; + writeStartElement(w, CSS_ATTR); + writeAttribute(w, NAME, f.name.getValue()); + w.writeEndElement(); } + else if (i instanceof ContentFunction) { + ContentFunction f = (ContentFunction)i; + writeStartElement(w, CSS_CONTENT); + if (f.target.isPresent()) + if (f.target.get().url != null) { + TermURI t = f.target.get().url; + URI url = URLs.asURI(t.getValue()); + if (t.getBase() != null) + url = URLs.resolve(URLs.asURI(t.getBase()), url); + writeAttribute(w, TARGET, url.toString()); + } else + writeAttribute(w, TARGET_ATTRIBUTE, f.target.get().urlAttr.name.getValue()); + w.writeEndElement(); } + else if (i instanceof StringFunction) { + StringFunction f = (StringFunction)i; + writeStartElement(w, CSS_STRING); + writeAttribute(w, NAME, f.name.getValue()); + if (f.target.isPresent()) + if (f.target.get().url != null) { + TermURI t = f.target.get().url; + URI url = URLs.asURI(t.getValue()); + if (t.getBase() != null) + url = URLs.resolve(URLs.asURI(t.getBase()), url); + writeAttribute(w, TARGET, url.toString()); + } else + writeAttribute(w, TARGET_ATTRIBUTE, f.target.get().urlAttr.name.getValue()); + else if (f.scope.isPresent()) + writeAttribute(w, SCOPE, f.scope.get().toString()); + w.writeEndElement(); } + else if (i instanceof CounterFunction) { + CounterFunction f = (CounterFunction)i; + writeStartElement(w, CSS_COUNTER); + writeAttribute(w, NAME, f.name.getValue()); + if (f.target.isPresent()) + if (f.target.get().url != null) { + TermURI t = f.target.get().url; + URI url = URLs.asURI(t.getValue()); + if (t.getBase() != null) + url = URLs.resolve(URLs.asURI(t.getBase()), url); + writeAttribute(w, TARGET, url.toString()); + } else + writeAttribute(w, TARGET_ATTRIBUTE, f.target.get().urlAttr.name.getValue()); + if (f.style.isPresent()) + writeAttribute(w, STYLE, toString(f.style.get())); + w.writeEndElement(); } + else if (i instanceof TextFunction) { + TextFunction f = (TextFunction)i; + writeStartElement(w, CSS_TEXT); + if (f.target.url != null) { + TermURI t = f.target.url; + URI url = URLs.asURI(t.getValue()); + if (t.getBase() != null) + url = URLs.resolve(URLs.asURI(t.getBase()), url); + writeAttribute(w, TARGET, url.toString()); + } else + writeAttribute(w, TARGET_ATTRIBUTE, f.target.urlAttr.name.getValue()); + w.writeEndElement(); } + else if (i instanceof LeaderFunction) { + LeaderFunction f = (LeaderFunction)i; + writeStartElement(w, CSS_LEADER); + writeAttribute(w, PATTERN, f.pattern.getValue()); + if (f.position.isPresent()) + writeAttribute(w, POSITION, toString(f.position.get())); + if (f.alignment.isPresent()) + writeAttribute(w, ALIGNMENT, f.alignment.get().toString()); + w.writeEndElement(); } + else if (i instanceof FlowFunction) { + FlowFunction f = (FlowFunction)i; + writeStartElement(w, CSS_FLOW); + writeAttribute(w, FROM, f.from.getValue()); + if (f.scope.isPresent()) + writeAttribute(w, SCOPE, f.scope.get().toString()); + w.writeEndElement(); } + else if (i instanceof TermFunction) { + TermFunction f = (TermFunction)i; + writeStartElement(w, CSS_CUSTOM_FUNC); + writeAttribute(w, NAME, f.getFunctionName()); + int k = 0; + for (Term arg : f) { + k++; + writeAttribute(w, new QName("arg" + k), toString(arg)); } + w.writeEndElement(); } + else + throw new RuntimeException("coding error"); + } + + public static void toXml(StringSetList list, XMLStreamWriter w) throws XMLStreamException { + for (Term i : list) { + if (i instanceof StringSetList.StringSet) { + StringSetList.StringSet s = (StringSetList.StringSet)i; + writeStartElement(w, CSS_STRING_SET); + writeAttribute(w, NAME, s.getKey()); + toXml(s.getValue(), w); + w.writeEndElement(); } + else + throw new RuntimeException("coding error"); + } + } + + public static void toXml(CounterSetList list, XMLStreamWriter w) throws XMLStreamException { + for (Term i : list) { + if (i instanceof CounterSetList.CounterSet) { + CounterSetList.CounterSet s = (CounterSetList.CounterSet)i; + writeStartElement(w, CSS_COUNTER_SET); + writeAttribute(w, NAME, s.getKey()); + writeAttribute(w, VALUE, "" + s.getValue()); + w.writeEndElement(); } + else + throw new RuntimeException("coding error"); + } + } } diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/BrailleCssStyle.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/BrailleCssStyle.java new file mode 100644 index 0000000000..88f750b743 --- /dev/null +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/BrailleCssStyle.java @@ -0,0 +1,1155 @@ +package org.daisy.pipeline.braille.css.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.SortedMap; +import java.util.TreeMap; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.Iterables; + +import cz.vutbr.web.css.CSSProperty; +import cz.vutbr.web.css.Declaration; +import cz.vutbr.web.css.Rule; +import cz.vutbr.web.css.RuleBlock; +import cz.vutbr.web.css.RuleMargin; +import cz.vutbr.web.css.RulePage; +import cz.vutbr.web.css.Selector; +import cz.vutbr.web.css.Selector.Combinator; +import cz.vutbr.web.css.Selector.PseudoClass; +import cz.vutbr.web.css.Selector.SelectorPart; +import cz.vutbr.web.css.SourceLocator; +import cz.vutbr.web.css.Term; + +import org.daisy.braille.css.BrailleCSSParserFactory.Context; +import org.daisy.braille.css.BrailleCSSProperty.Content; +import org.daisy.braille.css.BrailleCSSProperty.StringSet; +import org.daisy.braille.css.BrailleCSSProperty.TextTransform; +import org.daisy.braille.css.InlineStyle; +import org.daisy.braille.css.InlineStyle.RuleMainBlock; +import org.daisy.braille.css.InlineStyle.RuleRelativeBlock; +import org.daisy.braille.css.InlineStyle.RuleRelativeHyphenationResource; +import org.daisy.braille.css.InlineStyle.RuleRelativePage; +import org.daisy.braille.css.InlineStyle.RuleRelativeVolume; +import org.daisy.braille.css.PropertyValue; +import org.daisy.braille.css.SelectorImpl.PseudoElementImpl; +import org.daisy.braille.css.SimpleInlineStyle; +import org.daisy.braille.css.SupportedBrailleCSS; +import org.daisy.braille.css.RuleCounterStyle; +import org.daisy.braille.css.RuleHyphenationResource; +import org.daisy.braille.css.RuleTextTransform; +import org.daisy.braille.css.RuleVolume; +import org.daisy.braille.css.RuleVolumeArea; +import org.daisy.braille.css.VendorAtRule; +import org.daisy.pipeline.braille.css.impl.BrailleCssParser.ParsedDeclaration; +import org.daisy.pipeline.braille.css.impl.BrailleCssParser.ParsedDeclarations; +import org.daisy.pipeline.css.CounterStyle; + +import org.w3c.dom.Element; + +/** + * Tree reprentation of a CSS style. The tree structure is based on Sass' nesting of selectors. + * + * This class is immutable + */ +public final class BrailleCssStyle implements Cloneable { + + public final static BrailleCssStyle EMPTY = new Builder().build(); + + // these fields need to be package private because they are used in BrailleCssSerializer and BrailleCssParser + // note that even though the declarations are assumed to not change, we don't assume they are unmodifiable + Iterable declarations; + SortedMap nestedStyles; // sorted by key + // used in BrailleCssSerializer + BrailleCssParser parser; + Context context; + + public final Object underlyingObject; // - CounterStyle + // - null + + private BrailleCssStyle(Builder builder) { + this.underlyingObject = builder.underlyingObject; + if ((builder.parser != null && builder.parser.getSupportedBrailleCSS(builder.context).isPresent()) + || builder.declarations != null && Iterables.any(builder.declarations.values(), + d -> d instanceof PropertyValue)) { + this.declarations = new ParsedDeclarations(builder.parser, + builder.context, + builder.declarations != null + ? builder.declarations.values() + : null); + this.parser = ((ParsedDeclarations)this.declarations).getParser(); + this.context = ((ParsedDeclarations)this.declarations).getContext(); + } else { + if (builder.declarations == null || builder.declarations.isEmpty()) + this.declarations = null; + else + this.declarations = ImmutableList.copyOf(builder.declarations.values()); + this.parser = builder.parser; + this.context = builder.context; + } + this.nestedStyles = builder.nestedStyles != null && !builder.nestedStyles.isEmpty() + ? ImmutableSortedMap.copyOfSorted(builder.buildNestedStyles()) + : null; + } + + public boolean isEmpty() { + if (nestedStyles != null) + return false; + if (declarations instanceof ParsedDeclarations) + // a ParsedDeclarations is never really empty because it contains information about initial values + return false; + if (declarations == null) + return true; + if (Iterables.isEmpty(declarations)) + return true; + return false; + } + + private boolean evaluated = false; + + /** + * Evaluate attr() and content() values in content and + * string-set properties. + */ + public BrailleCssStyle evaluate(Element context) { + if (evaluated) return this; + BrailleCssStyle copy = null; + if (declarations != null) { + Iterable evaluatedDeclarations = evaluateDeclarations(context); + if (copy == null && evaluatedDeclarations != declarations) + copy = clone(); + if (copy != null) { + copy.declarations = evaluatedDeclarations; + copy.declarationMap = null; + } + } + if (nestedStyles != null) { + SortedMap nestedStylesCopy = new TreeMap<>(sortSelectors); + for (Map.Entry e : nestedStyles.entrySet()) { + BrailleCssStyle nestedStyle = e.getValue(); + BrailleCssStyle nestedStyleEvaluated = nestedStyle.evaluate(context); + if (copy == null && nestedStyleEvaluated != nestedStyle) + copy = clone(); + nestedStylesCopy.put(e.getKey(), nestedStyleEvaluated); + } + if (copy != null) + copy.nestedStyles = nestedStylesCopy; + } + if (copy != null) { + copy.serialized = null; + copy.evaluated = true; + return copy; + } else + return this; + } + + private Iterable evaluateDeclarations(Element context) { + Iterable copy = null; + if (declarations != null) + for (Declaration d : declarations) { + if (d instanceof PropertyValue) { + PropertyValue pv = (PropertyValue)d; + CSSProperty p = pv.getCSSProperty(); + if (p == Content.content_list) { + Term v = pv.getValue(); + if (v instanceof ContentList) { + // clone because evaluate() mutates the value + if (copy == null) + copy = copyDeclarations(); + // find value in cloned declarations + for (Declaration dd : copy) + if (((PropertyValue)dd).getCSSProperty() == Content.content_list) { + ((ContentList)((PropertyValue)dd).getValue()).evaluate(context); + break; } + } else + throw new IllegalStateException(); // coding error + } else if (p == StringSet.list_values) { + Term v = pv.getValue(); + if (v instanceof StringSetList) { + // clone because evaluate() mutates the value + if (copy == null) + copy = copyDeclarations(); + // find value in cloned declarations + for (Declaration dd : copy) + if (((PropertyValue)dd).getCSSProperty() == StringSet.list_values) { + ((StringSetList)((PropertyValue)dd).getValue()).evaluate(context); + break; } + } else + throw new IllegalStateException(); // coding error + } + } + } + if (copy != null) + return copy; + else + return declarations; + } + + // also used in BrailleCssParser + static Declaration evaluateDeclaration(Declaration declaration, Element context) { + if (!(declaration instanceof PropertyValue)) + return declaration; + PropertyValue pv = (PropertyValue)declaration; + PropertyValue copy = null; + CSSProperty p = pv.getCSSProperty(); + if (p == Content.content_list) { + if (pv.getValue() instanceof ContentList) { + // clone because evaluate() mutates the value + copy = (PropertyValue)declaration.clone(); + ((ContentList)copy.getValue()).evaluate(context); + } else + throw new IllegalStateException(); // coding error + } else if (p == StringSet.list_values) { + if (pv.getValue() instanceof StringSetList) { + // clone because evaluate() mutates the value + copy = (PropertyValue)declaration.clone(); + ((StringSetList)copy.getValue()).evaluate(context); + } else + throw new IllegalStateException(); // coding error + } + if (copy != null) + return copy; + else + return declaration; + } + + /** + * @return a deep copy of the {@code declarations} field + */ + private Iterable copyDeclarations() { + if (declarations instanceof ParsedDeclarations) + // make sure that declarations field stays a ParsedDeclarations object because + // it is assumed throughout the code + return (ParsedDeclarations)((ParsedDeclarations)declarations).clone(); + else { + List declarationsCopy = new ArrayList<>(); + for (Declaration dd : declarations) + declarationsCopy.add((Declaration)dd.clone()); + return ImmutableList.copyOf(declarationsCopy); + } + } + + private Map declarationMap = null; + + private Map getDeclarationMap() { + if (declarations != null) { + synchronized (this) { + if (declarationMap == null) { + declarationMap = new TreeMap<>(); + for (Declaration d : declarations) + declarationMap.put(d.getProperty(), d); + } + } + } + return declarationMap; + } + + public Iterable getPropertyNames() { + if (declarations != null) + return getDeclarationMap().keySet(); + else + return Collections.emptyList(); + } + + /** + * Caller must guarantee that the {@link Declaration} objects will not be modified. + */ + public Iterable getDeclarations() { + if (declarations != null) + return getDeclarationMap().values(); + else + return Collections.emptyList(); + } + + public Iterable getSelectors() { + if (nestedStyles != null) + return nestedStyles.keySet(); + else + return Collections.emptyList(); + } + + private List rules = null; + + public Iterable getRules() { + if (nestedStyles != null) { + synchronized (this) { + if (rules == null) { + rules = new ArrayList<>(); + for (Map.Entry e : nestedStyles.entrySet()) { + String selector = e.getKey(); + rules.add(new Builder().add(selector, e.getValue()).build()); + } + } + } + return rules; + } else + return Collections.emptyList(); + } + + /** + * Return the style as a {@link SimpleInlineStyle} object. + * + * @param mutable Whether the caller wishes to mutate the returned object. + * @throws UnsupportedOperationException if this is not a simple inline style + */ + public SimpleInlineStyle asSimpleInlineStyle(boolean mutable) { + if (nestedStyles != null) + throw new UnsupportedOperationException("not a simple inline style"); + else if (declarations == null) + return SimpleInlineStyle.EMPTY; + else if (context != Context.ELEMENT) + throw new UnsupportedOperationException("not a simple inline style"); + else if (!(declarations instanceof ParsedDeclarations)) + throw new IllegalStateException(); // coding error + ParsedDeclarations s = (ParsedDeclarations)declarations; + if (mutable && !s.isEmpty()) { + s = (ParsedDeclarations)s.clone(); // make a deep copy + for (Declaration d : s) { + if (d instanceof PropertyValue) { + PropertyValue pv = (PropertyValue)d; + CSSProperty p = pv.getCSSProperty(); + if (p == TextTransform.list_values) { + Term v = pv.getValue(); + if (v instanceof TextTransformList) + ((TextTransformList)v).locked = false; + break; + } + } + } + // mark as mutable + s.locked = false; + } + return s; + } + + // used in BrailleCssParser + static Declaration unlockDeclaration(Declaration declaration) { + if (!(declaration instanceof PropertyValue)) + return declaration; + PropertyValue pv = (PropertyValue)declaration; + if (pv.getCSSProperty() == TextTransform.list_values) { + if (pv.getValue() instanceof TextTransformList) { + // clone because we're going to make the value mutable + pv = (PropertyValue)pv.clone(); + ((TextTransformList)pv.getValue()).locked = false; + } else + throw new IllegalStateException(); // coding error + } + return pv; + } + + /** + * Get the declaration for the given property name. + * + * Caller must guarantee that the object will not be modified. + */ + public Declaration getDeclaration(String propertyName) { + return getDeclaration(propertyName, false); + } + + /** + * @param includeInitial Whether to include the initial value if the style does not contain the property. + */ + public Declaration getDeclaration(String propertyName, boolean includeInitial) { + if (declarations != null) { + Declaration d = getDeclarationMap().get(propertyName); + if (d != null) + return d; + } + if (includeInitial && declarations instanceof ParsedDeclarations) + return ((ParsedDeclarations)declarations).getOrDefault(propertyName); + return null; + } + + /** + * Get the nested style for the given selector. + */ + public BrailleCssStyle getNestedStyle(String selector) { + if (nestedStyles != null) { + BrailleCssStyle n = nestedStyles.get(selector); + if (n != null) + return n; + } + if (parser != null) + switch (context) { + case ELEMENT: + // return empty @page rule rather than null, in order to make it possible to + // get default page property values using s:getOrDefault() + if ("@page".equals(selector)) + // by passing parser this object will not be empty (because it contains + // information about initial values, see #isEmpty()) + return new Builder(parser, Context.PAGE).build(); + break; + default: + break; + } + return null; + } + + /** + * Remove declaration if key is a property name, or nested style if key is a selector. + */ + public BrailleCssStyle remove(String key) { + if (getDeclaration(key) != null || + nestedStyles != null && nestedStyles.containsKey(key)) + return new Builder(this).remove(key).build(); + else + return this; + } + + /** + * For each provided key, remove declaration if key is a property name, or rule if key is a selector. + */ + public BrailleCssStyle remove(Iterator keys) { + if (keys.hasNext()) { + Builder b = new Builder(this); + while (keys.hasNext()) { + String key = keys.next(); + if (getDeclaration(key) != null || + nestedStyles != null && nestedStyles.containsKey(key)) + b = b.remove(key); + } + return b.build(); + } + return this; + } + + /** + * Add other styles to this style. Properties are overwritten by properties declared in + * following style items. + * + * @param styles objects must be of type {@link BrailleCssStyle} or {@link Declaration}. + */ + public BrailleCssStyle add(Iterator styles) { + Builder b = null; + while (styles.hasNext()) { + Object s = styles.next(); + if (s instanceof BrailleCssStyle) { + if (((BrailleCssStyle)s).isEmpty()) + continue; + if (b == null) b = new Builder(this); + b.add((BrailleCssStyle)s); + } else if (s instanceof Declaration) { + if (b == null) b = new Builder(this); + b.add((Declaration)s); + } else + throw new IllegalArgumentException(); + } + if (b != null) + return b.build(); + else + return this; + } + + /** + * Add a declaration. If a declaration for the same property already exists, it is overwritten. + */ + public BrailleCssStyle add(Declaration declaration) { + Builder b = new Builder(this); + b.add(declaration); + return b.build(); + } + + /** + * @param contentList assumed to not change + */ + public BrailleCssStyle add(String propertyName, ContentList contentList) { + Builder b = new Builder(this); + b.add(new ParsedDeclaration(contentList.getParser(), + contentList.getContext(), + propertyName, + Content.content_list, + contentList, + null)); + return b.build(); + } + + /** + * Add a rule. If a rule with the same selector already exists, the rules are merged. Properties + * in the existing rule are overwritten by properties in the new rule. + */ + public BrailleCssStyle add(String key, BrailleCssStyle nestedStyle) { + return new Builder(this).add(key, nestedStyle).build(); + } + + @Override + public BrailleCssStyle clone() { + try { + return (BrailleCssStyle)super.clone(); + } catch (CloneNotSupportedException e) { + throw new InternalError("coding error"); + } + } + + // for BrailleCssSerializer + String serialized = null; + + @Override + public String toString() { + return BrailleCssSerializer.toString(this); + } + + // used in BrailleCssSerializer + BrailleCssStyle relativize(BrailleCssStyle base) { + if (base.declarations != null && !(base.declarations instanceof ParsedDeclarations)) + throw new IllegalArgumentException(); + return relativize((ParsedDeclarations)base.declarations).build(); + } + + private Builder relativize(ParsedDeclarations base) { + if (declarations != null && !(declarations instanceof ParsedDeclarations)) + throw new IllegalArgumentException(); + Builder relative = new Builder(this); + if (base != null) + for (PropertyValue p : base) { + if (p.getCSSProperty().inherited()) { + PropertyValue pp = (PropertyValue)getDeclaration(p.getProperty()); + if (pp != null) { + if (equal(p, pp)) + relative.remove(p.getProperty()); + } else { + PropertyValue def = p.getDefault(); + if (def != null && !equalIgnoreSource(p, def)) + relative.add(def); + } + } + } + // omit properties that have the default value and are not inherited or don't exist in the parent style + if (declarations != null) + for (Declaration d : getDeclarationMap().values()) { + PropertyValue p = (PropertyValue)d; + PropertyValue def = p.getDefault(); + if ((!p.getCSSProperty().inherited() || base == null || base.getProperty(p.getProperty()) == null) && equal(p, def)) + relative.remove(p.getProperty()); + } + // relativize nested styles + BrailleCssStyle main = null; + for (String sel : getSelectors()) { + if (sel.startsWith("@")) + // skip at-rules + continue; + BrailleCssStyle nested = getNestedStyle(sel); + if (sel.equals("&::before") || + sel.equals("&::after") || + (sel.startsWith("&:") && !sel.startsWith("&::"))) { + // pseudo-classes and ::before and ::after pseudo-elements inherit from main style + if (relative.declarations != null && !relative.declarations.isEmpty()) { + relative.remove(sel); + if (main == null) main = relative.build(); + if (main.declarations != null && !(main.declarations instanceof ParsedDeclarations)) + throw new IllegalStateException(); + relative.add(sel, nested.relativize((ParsedDeclarations)main.declarations)); + } + } else { + relative.remove(sel); + relative.add(sel, nested.relativize(base)); + } + } + return relative; + } + + @Override + public boolean equals(Object other) { + if (this == other) + return true; + if (!(other instanceof BrailleCssStyle)) + return false; + BrailleCssStyle that = (BrailleCssStyle)other; + if (declarations == null) + return that.declarations == null; + else if (that.declarations == null) + return false; + if (declarations instanceof ParsedDeclarations) { + if (!(that.declarations instanceof ParsedDeclarations)) + return false; + if (((ParsedDeclarations)that.declarations).getParser() != ((ParsedDeclarations)declarations).getParser()) + return false; + if (((ParsedDeclarations)that.declarations).getContext() != ((ParsedDeclarations)declarations).getContext()) + return false; + } else if (that.declarations instanceof ParsedDeclarations) + return false; + // using toString() for efficiency, because toString() is memoized + return toString().equals(that.toString()); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + private static boolean equal(PropertyValue a, PropertyValue b) { + return equal(a, b, false); + } + + private static boolean equalIgnoreSource(PropertyValue a, PropertyValue b) { + return equal(a, b, true); + } + + private static boolean equal(PropertyValue a, PropertyValue b, boolean ignoreSource) { + if (a == null) + return b == null; + else if (b == null) + return false; + else { + if (a.getCSSProperty() != b.getCSSProperty()) + return false; + Term va = a.getValue(); + Term vb = b.getValue(); + if (!equal(va != null ? BrailleCssSerializer.toString(va) : null, + vb != null ? BrailleCssSerializer.toString(vb) : null)) + return false; + if (!ignoreSource && !equal(a.getSource(), b.getSource())) + return false; + } + return true; + } + + private static boolean equal(SourceLocator a, SourceLocator b) { + if (a != null && a.getURL() == null) + a = null; + if (b != null && b.getURL() == null) + b = null; + if (a == null) + return b == null; + else if (b == null) + return false; + else if (!equal(a.getURL(), b.getURL())) + return false; + else if (a.getLineNumber() != b.getLineNumber()) + return false; + else if (a.getColumnNumber() != b.getColumnNumber()) + return false; + else + return true; + } + + private static boolean equal(Object a, Object b) { + if (a == null) + return b == null; + else if (b == null) + return false; + else + return a.equals(b); + } + + private static class Builder { + + private final BrailleCssParser parser; + private final Context context; + private final Object underlyingObject; + private Map declarations; + private Map nestedStyles; // values are instances of Builder or BrailleCssStyle + + /** + * @param context null means neither of { ELEMENT, PAGE, VOLUME, TEXT_TRANSFORM, COUNTER_STYLE, VENDOR_RULE } + */ + private Builder(BrailleCssParser parser, Context context) { + this(parser, context, null); + } + + private Builder(BrailleCssParser parser, Context context, Object underlyingObject) { + this.parser = parser; + this.context = context; + this.underlyingObject = underlyingObject; + } + + /** + * Create a non-parsing Builder. + */ + private Builder() { + this(null, null, null); + } + + private Builder(Object underlyingObject) { + this(null, null, underlyingObject); + } + + private Builder(BrailleCssStyle style) { + this(style.parser, style.context, style.underlyingObject); + add(style); + } + + /** + * @param declaration assumed to not change + */ + private Builder add(Declaration declaration) { + if (declaration != null) { + if (declarations == null) declarations = new TreeMap<>(); + declarations.put(declaration.getProperty(), declaration); + } + return this; + } + + private Builder add(String selector, Object nestedStyle) { + if (!(nestedStyle instanceof BrailleCssStyle || nestedStyle instanceof Builder)) + throw new IllegalArgumentException(); + if (this.nestedStyles == null) this.nestedStyles = new TreeMap(); + if (selector == null) { + if (nestedStyle instanceof BrailleCssStyle) + add((BrailleCssStyle)nestedStyle); + else + add((Builder)nestedStyle); + } else if (this.nestedStyles.containsKey(selector)) { + Object s = this.nestedStyles.get(selector); + if (s instanceof Builder) { + if (nestedStyle instanceof BrailleCssStyle) + ((Builder)s).add((BrailleCssStyle)nestedStyle); + else + ((Builder)s).add((Builder)nestedStyle); + } else { + if (nestedStyle instanceof BrailleCssStyle) + this.nestedStyles.put(selector, new Builder((BrailleCssStyle)s).add((BrailleCssStyle)nestedStyle)); + else + this.nestedStyles.put(selector, new Builder((BrailleCssStyle)s).add((Builder)nestedStyle)); + } + } else + this.nestedStyles.put(selector, nestedStyle); + return this; + } + + private Builder add(BrailleCssStyle style) { + if (style.declarations != null) + for (Declaration d : style.declarations) + add(d); + if (style.nestedStyles != null) + for (Map.Entry e : style.nestedStyles.entrySet()) + add(e.getKey(), e.getValue()); + return this; + } + + private Builder add(Builder style) { + if (style.declarations != null) + for (Declaration d : style.declarations.values()) + add(d); + if (style.nestedStyles != null) + for (Map.Entry e : style.nestedStyles.entrySet()) { + Object s = e.getValue(); + if (s instanceof Builder) + add(e.getKey(), (Builder)s); + else + add(e.getKey(), (BrailleCssStyle)s); + } + return this; + } + + /** + * @param key property name or selector + */ + private Builder remove(String key) { + if (this.declarations != null && this.declarations.remove(key) != null) + ; + else if (this.nestedStyles != null) + this.nestedStyles.remove(key); + return this; + } + + private BrailleCssStyle build() { + return new BrailleCssStyle(this); + } + + private SortedMap buildNestedStyles() { + if (nestedStyles == null) + return null; + SortedMap map = null; + for (Map.Entry e : nestedStyles.entrySet()) { + Object v = e.getValue(); + BrailleCssStyle s = v instanceof Builder ? ((Builder)v).build() : (BrailleCssStyle)v; + if (!s.isEmpty()) { + if (map == null) + map = new TreeMap(sortSelectors); + map.put(e.getKey(), s); + } + } + return map; + } + } + + /** + * @param declarations assumed to not change + */ + public static BrailleCssStyle of(SimpleInlineStyle declarations) { + if (declarations == SimpleInlineStyle.EMPTY) + return BrailleCssStyle.EMPTY; + else if (declarations instanceof ParsedDeclarations) { + BrailleCssStyle style = new Builder().build(); + style.declarations = declarations; + style.parser = ((ParsedDeclarations)declarations).getParser(); + style.context = ((ParsedDeclarations)declarations).getContext(); + ((ParsedDeclarations)declarations).locked = true; + return style; + } else { + // assuming that declarations originate from a ParsedDeclarations + Builder b = new Builder(); + for (PropertyValue d : declarations) + b.add(d); + return b.build(); + } + } + + /** + * @param declaration assumed to not change + */ + public static BrailleCssStyle of(Declaration declaration) { + Builder style = new Builder(); + style.add(declaration); + return style.build(); + } + + /** + * @param page assumed to not change + * @param parser {@link BrailleCssParser} for declarations inside page and margin area rules. + */ + // also used in BrailleCssSerializer.toString(RulePage, BrailleCssParser) + static BrailleCssStyle of(BrailleCssParser parser, RulePage page) { + return of(parser, page, false); + } + + private static BrailleCssStyle of(BrailleCssParser parser, RuleRelativePage page) { + return of(parser, page.asRulePage(), true); + } + + private static BrailleCssStyle of(BrailleCssParser parser, RulePage page, boolean relative) { + // assumed to be anonymous page + Builder style = new Builder(parser, Context.PAGE); + for (Rule r : page) + if (r instanceof Declaration) + style.add((Declaration)r); + else if (r instanceof RuleMargin) { + Builder margin = new Builder(parser, Context.PAGE); + for (Declaration d : (RuleMargin)r) + margin.add(d); + style.add("@" + ((RuleMargin)r).getMarginArea(), margin); + } else + throw new RuntimeException("coding error"); + String pseudo = page.getPseudo(); + Builder relativeRule = (pseudo == null) + ? style + : new Builder(parser, Context.PAGE).add("&:" + pseudo, style); + if (relative) + return relativeRule.build(); + else + return new Builder().add("@page", relativeRule).build(); + } + + /** + * @param volumeArea assumed to not change + */ + private static BrailleCssStyle of(BrailleCssParser parser, RuleVolumeArea volumeArea) { + Builder style = new Builder(parser, Context.VOLUME); + for (Rule r : volumeArea) + if (r instanceof Declaration) + style.add((Declaration)r); + else if (r instanceof RulePage) + style.add(of(parser, (RulePage)r)); + else + throw new RuntimeException("coding error"); + return new Builder().add("@" + volumeArea.getVolumeArea().value, style).build(); + } + + /** + * @param volume assumed to not change + */ + private static BrailleCssStyle of(BrailleCssParser parser, RuleVolume volume) { + return of(parser, volume, false); + } + + private static BrailleCssStyle of(BrailleCssParser parser, RuleRelativeVolume volume) { + return of(parser, volume.asRuleVolume(), true); + } + + private static BrailleCssStyle of(BrailleCssParser parser, RuleVolume volume, boolean relative) { + Builder style = new Builder(parser, Context.VOLUME); + for (Rule r : volume) + if (r instanceof Declaration) + style.add((Declaration)r); + else if (r instanceof RuleVolumeArea) + style.add(of(parser, (RuleVolumeArea)r)); + else + throw new RuntimeException("coding error"); + String pseudo = volume.getPseudo(); + Builder relativeRule = (pseudo == null) + ? style + : new Builder(parser, Context.VOLUME).add("&:" + pseudo, style); + if (relative) + return relativeRule.build(); + else + return new Builder().add("@volume", relativeRule).build(); + } + + /** + * @param hyphenationResource assumed to not change + */ + private static BrailleCssStyle of(BrailleCssParser parser, RuleHyphenationResource hyphenationResource) { + return of(parser, hyphenationResource, false); + } + + private static BrailleCssStyle of(BrailleCssParser parser, RuleRelativeHyphenationResource hyphenationResource) { + return of(parser, hyphenationResource.asRuleHyphenationResource(), true); + } + + private static BrailleCssStyle of(BrailleCssParser parser, RuleHyphenationResource hyphenationResource, boolean relative) { + Builder style = new Builder(parser, Context.HYPHENATION_RESOURCE); + for (Declaration d : hyphenationResource) + style.add(d); + Builder relativeRule = new Builder(parser, Context.HYPHENATION_RESOURCE) + .add("&:lang(" + BrailleCssSerializer.serializeLanguageRanges(hyphenationResource.getLanguageRanges()) + ")", style); + if (relative) + return relativeRule.build(); + else + return new Builder().add("@hyphenation-resource", relativeRule).build(); + } + + /** + * @param rule assumed to not change + */ + private static BrailleCssStyle of(BrailleCssParser parser, VendorAtRule> rule) { + Builder style = new Builder(); + for (Rule r : rule) + if (r instanceof Declaration) + style.add((Declaration)r); + else if (r instanceof VendorAtRule) + style.add("@" + ((VendorAtRule)r).getName(), of(parser, (VendorAtRule>)r)); + else + throw new RuntimeException("coding error"); + return style.build(); + } + + /** + * @param inlineStyle assumed to not change + */ + private static BrailleCssStyle of(BrailleCssParser parser, Context context, InlineStyle inlineStyle) { + Builder style = new Builder(parser, context); + Map counterStyleRules = null; + for (RuleBlock rule : inlineStyle) { + if (rule instanceof RuleMainBlock) + // Note that the declarations have not been transformed by SupportedBrailleCSS yet. + // This will be done when build() is called. + for (Declaration d : (RuleMainBlock)rule) + style.add(d); + else if (rule instanceof RuleRelativeBlock) { + String[] selector = serializeSelector(((RuleRelativeBlock)rule).getSelector()); + if (context == Context.COUNTER_STYLE && selector.length == 1 && selector[0].startsWith("& ")) { + String name = selector[0].substring(2); + RuleCounterStyle counterStyle = new RuleCounterStyle(name); { + for (Rule r : (RuleRelativeBlock)rule) + if (!(r instanceof Declaration)) + throw new RuntimeException(); // @page can not occur inside @counter-style + counterStyle.replaceAll((RuleBlock)rule); } + if (counterStyleRules == null) + counterStyleRules = new HashMap<>(); + counterStyleRules.put(name, counterStyle); + } else { + Builder decls = new Builder(parser, context); { + for (Rule r : (RuleRelativeBlock)rule) + if (r instanceof Declaration) + decls.add((Declaration)r); + else if (r instanceof RulePage) + decls.add(of(parser, (RulePage)r)); + else + throw new RuntimeException("coding error"); + } + if (selector.length > 0) { + for (int i = selector.length - 1; i > 0; i--) + decls = new Builder(parser, context).add(selector[i], decls); + style.add(selector[0], decls); + } + } + } else if (rule instanceof RulePage) + style.add(of(parser, (RulePage)rule)); + else if (rule instanceof RuleVolume) + style.add(of(parser, (RuleVolume)rule)); + else if (rule instanceof RuleTextTransform) { + String name = ((RuleTextTransform)rule).getName(); + Builder textTransform = new Builder(parser, Context.TEXT_TRANSFORM); + for (Declaration d : (RuleTextTransform)rule) + textTransform.add(d); + if (name == null) + style.add("@text-transform", textTransform); + else + style.add("@text-transform", new Builder(parser, Context.TEXT_TRANSFORM).add("& " + name, textTransform)); } + else if (rule instanceof RuleHyphenationResource) + style.add(of(parser, (RuleHyphenationResource)rule)); + else if (rule instanceof RuleCounterStyle) { + if (counterStyleRules == null) + counterStyleRules = new HashMap<>(); + counterStyleRules.put(((RuleCounterStyle)rule).getName(), (RuleCounterStyle)rule); } + else if (rule instanceof RuleMargin) { + Builder margin = new Builder(parser, Context.PAGE); + for (Declaration d : (RuleMargin)rule) + margin.add(d); + style.add("@" + ((RuleMargin)rule).getMarginArea(), margin); + } else if (rule instanceof RuleVolumeArea) + style.add(of(parser, (RuleVolumeArea)rule)); + else if (rule instanceof RuleRelativePage) + style.add(of(parser, (RuleRelativePage)rule)); + else if (rule instanceof RuleRelativeVolume) + style.add(of(parser, (RuleRelativeVolume)rule)); + else if (rule instanceof RuleRelativeHyphenationResource) + style.add(of(parser, (RuleRelativeHyphenationResource)rule)); + else if (rule instanceof VendorAtRule) + style.add("@" + ((VendorAtRule)rule).getName(), of(parser, (VendorAtRule>)rule)); + else + throw new RuntimeException("coding error"); + } + if (counterStyleRules != null) { + for (Map.Entry e : CounterStyle.parseCounterStyleRules(counterStyleRules.values()).entrySet()) { + Builder counterStyle = new Builder(parser, Context.COUNTER_STYLE, e.getValue()); + for (Declaration d : counterStyleRules.get(e.getKey())) + counterStyle.add(d); + if (context == Context.COUNTER_STYLE) + style.add("& " + e.getKey(), counterStyle); + else + style.add("@counter-style", new Builder(parser, Context.COUNTER_STYLE).add("& " + e.getKey(), counterStyle)); + } + } + return style.build(); + } + + // used in BrailleCssParser + static BrailleCssStyle of(BrailleCssParser parser, Context context, String inlineStyle) { + if (context == null) + throw new IllegalArgumentException(); + return of(parser, context, new InlineStyle(inlineStyle, context, null, + parser.getBrailleCSSParserFactory())); + } + + // used in BrailleCssParser + BrailleCssStyle inheritFrom(ParsedDeclarations base) { + if (declarations != null && !(declarations instanceof ParsedDeclarations)) + throw new IllegalStateException(); + ParsedDeclarations decls = declarations != null + ? (ParsedDeclarations)declarations + : ParsedDeclarations.EMPTY; + decls = decls.inheritFrom(base); + SortedMap nested = null; + if (nestedStyles != null) + for (String sel : nestedStyles.keySet()) + if (!sel.startsWith("@")) { + BrailleCssStyle n = nestedStyles.get(sel); + BrailleCssStyle nn; { + if (sel.equals("&::before") || + sel.equals("&::after") || + (sel.startsWith("&:") && !sel.startsWith("&::"))) + // pseudo-classes and ::before and ::after pseudo-elements inherit from main style + nn = n.inheritFrom(decls); + else + nn = n.inheritFrom(base); + } + if (n != nn) { + if (nested == null) + nested = new TreeMap<>(nestedStyles); + nested.put(sel, nn); + } + } + if (declarations != null || !decls.isEmpty() || nested != null) { + BrailleCssStyle copy = clone(); + copy.declarations = decls.isEmpty() ? new ParsedDeclarations(parser, context, null) : decls; + copy.declarationMap = null; + if (nested != null) { + copy.nestedStyles = nested; + copy.rules = null; + } + copy.serialized = null; + return copy; + } + return this; + } + + // note that this BrailleCssStyle will never be cached because the context is unknown + public static BrailleCssStyle of(String selector, BrailleCssStyle style) { + return new Builder().add(selector, style).build(); + } + + private static final Comparator sortSelectors = new Comparator() { + public int compare(String selector1, String selector2) { + if (selector1.startsWith("&") && !selector2.startsWith("&")) + return 1; + else if (!selector1.startsWith("&") && selector2.startsWith("&")) + return -1; + else + return selector1.compareTo(selector2); + } + }; + + /* Split the selector parts and serialize */ + private static String[] serializeSelector(List combinedSelector) { + List selector = new ArrayList<>(); + for (CombinatorSelectorPart part : flattenSelector(combinedSelector)) + selector.add("&" + part); + return selector.toArray(new String[selector.size()]); + } + + /* Convert a combined selector, which may contain pseudo element parts with other pseudo + * elements stacked onto them, into a flat list of selector parts and combinators */ + private static List flattenSelector(List combinedSelector) { + List selector = new ArrayList<>(); + for (Selector s : combinedSelector) + flattenSelector(selector, s); + return selector; + } + + private static void flattenSelector(List collect, Selector selector) { + Combinator combinator = selector.getCombinator(); + for (SelectorPart part : selector) { + flattenSelector(collect, combinator, part); + combinator = null; + } + } + + private static void flattenSelector(List collect, Combinator combinator, SelectorPart part) { + collect.add(new CombinatorSelectorPart(combinator, part)); + if (part instanceof PseudoElementImpl) { + PseudoElementImpl pe = (PseudoElementImpl)part; + if (!pe.getCombinedSelectors().isEmpty()) + for (Selector s: pe.getCombinedSelectors()) + flattenSelector(collect, s); + else { + if (!pe.getPseudoClasses().isEmpty()) + for (PseudoClass pc : pe.getPseudoClasses()) + collect.add(new CombinatorSelectorPart(null, pc)); + if (pe.hasStackedPseudoElement()) + flattenSelector(collect, null, pe.getStackedPseudoElement()); + } + } + } + + private static class CombinatorSelectorPart { + final Combinator combinator; + final SelectorPart selector; + CombinatorSelectorPart(Combinator combinator, SelectorPart selector) { + this.combinator = combinator; + this.selector = selector; + } + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + if (combinator != null) + b.append(combinator.value()); + if (selector instanceof PseudoElementImpl) { + PseudoElementImpl pe = (PseudoElementImpl)selector; + b.append(":"); + if (!pe.isSpecifiedAsClass()) + b.append(":"); + b.append(pe.getName()); + String[] args = pe.getArguments(); + if (args.length > 0) { + b.append("("); + for (int i = 0; i < args.length; i++) { + if (i > 0) b.append(", "); + b.append(args[i]); + } + b.append(")"); + } + } else + b.append(selector); + return b.toString(); + } + } +} diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/BrailleCssTreeBuilder.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/BrailleCssTreeBuilder.java deleted file mode 100644 index 86b0d41868..0000000000 --- a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/BrailleCssTreeBuilder.java +++ /dev/null @@ -1,293 +0,0 @@ -package org.daisy.pipeline.braille.css.impl; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; - -import cz.vutbr.web.css.Declaration; -import cz.vutbr.web.css.Rule; -import cz.vutbr.web.css.RuleBlock; -import cz.vutbr.web.css.RuleMargin; -import cz.vutbr.web.css.RulePage; -import cz.vutbr.web.css.Selector; -import cz.vutbr.web.css.Selector.Combinator; -import cz.vutbr.web.css.Selector.PseudoClass; -import cz.vutbr.web.css.Selector.SelectorPart; - -import org.daisy.braille.css.AnyAtRule; -import org.daisy.braille.css.InlineStyle; -import org.daisy.braille.css.InlineStyle.RuleMainBlock; -import org.daisy.braille.css.InlineStyle.RuleRelativeBlock; -import org.daisy.braille.css.InlineStyle.RuleRelativeHyphenationResource; -import org.daisy.braille.css.InlineStyle.RuleRelativePage; -import org.daisy.braille.css.InlineStyle.RuleRelativeVolume; -import org.daisy.braille.css.SelectorImpl.PseudoElementImpl; -import org.daisy.braille.css.RuleCounterStyle; -import org.daisy.braille.css.RuleHyphenationResource; -import org.daisy.braille.css.RuleTextTransform; -import org.daisy.braille.css.RuleVolume; -import org.daisy.braille.css.RuleVolumeArea; - -public final class BrailleCssTreeBuilder { - - private BrailleCssTreeBuilder() {} - - public static class Style implements Comparator { - - List declarations; - SortedMap nestedStyles; // sorted by key - - public int compare(String selector1, String selector2) { - if (selector1.startsWith("&") && !selector2.startsWith("&")) - return 1; - else if (!selector1.startsWith("&") && selector2.startsWith("&")) - return -1; - else - return selector1.compareTo(selector2); - } - - private Style add(Declaration declaration) { - if (declaration != null) { - if (this.declarations == null) this.declarations = new ArrayList(); - declarations.add(declaration); - } - return this; - } - - private Style add(Iterable declarations) { - if (declarations != null) - for (Declaration d : declarations) - add(d); - return this; - } - - Style add(String selector, Style nestedStyle) { - if (this.nestedStyles == null) this.nestedStyles = new TreeMap(this); - if (selector == null) - add(nestedStyle); - if (this.nestedStyles.containsKey(selector)) - this.nestedStyles.get(selector).add(nestedStyle); - else - this.nestedStyles.put(selector, nestedStyle); - return this; - } - - Style add(String[] selector, Style nestedStyle) { - if (selector.length == 0) - return this; - for (int i = selector.length - 1; i > 0; i--) - nestedStyle = new Style().add(selector[i], nestedStyle); - return add(selector[0], nestedStyle); - } - - private Style add(Style style) { - add(style.declarations); - if (style.nestedStyles != null) - for (Map.Entry e : style.nestedStyles.entrySet()) - add(e.getKey(), e.getValue()); - return this; - } - - static Style of(RulePage page) { - // assumed to be anonymous page - Style style = new Style(); - for (Rule r : page) - if (r instanceof Declaration) - style.add((Declaration)r); - else if (r instanceof RuleMargin) - style.add("@" + ((RuleMargin)r).getMarginArea(), - new Style().add((List)r)); - else - throw new RuntimeException("coding error"); - String pseudo = page.getPseudo(); - return pseudo == null - ? style - : new Style().add("&:" + pseudo, style); - } - - private static Style of(RuleVolumeArea volumeArea) { - Style style = new Style(); - for (Rule r : volumeArea) - if (r instanceof Declaration) - style.add((Declaration)r); - else if (r instanceof RulePage) - style.add("@page", Style.of((RulePage)r)); - else - throw new RuntimeException("coding error"); - return style; - } - - private static Style of(RuleVolume volume) { - Style style = new Style(); - for (Rule r : volume) - if (r instanceof Declaration) - style.add((Declaration)r); - else if (r instanceof RuleVolumeArea) - style.add("@" + ((RuleVolumeArea)r).getVolumeArea().value, Style.of((RuleVolumeArea)r)); - else - throw new RuntimeException("coding error"); - String pseudo = volume.getPseudo(); - return pseudo == null - ? style - : new Style().add("&:" + pseudo, style); - } - - private static Style of(RuleHyphenationResource rule) { - return new Style().add("&:lang(" - + BrailleCssSerializer.serializeLanguageRanges(rule.getLanguageRanges()) - + ")", - new Style().add((List)rule)); - } - - private static Style of(AnyAtRule rule) { - Style style = new Style(); - for (Rule r : rule) - if (r instanceof Declaration) - style.add((Declaration)r); - else if (r instanceof AnyAtRule) - style.add("@" + ((AnyAtRule)r).getName(), Style.of((AnyAtRule)r)); - else - throw new RuntimeException("coding error"); - return style; - } - - public static Style of(InlineStyle inlineStyle) { - Style style = new Style(); - for (RuleBlock rule : inlineStyle) { - if (rule instanceof RuleMainBlock) - // Note that the declarations have not been transformed by BrailleCSSDeclarationTransformer yet - style.add((List)rule); - else if (rule instanceof RuleRelativeBlock) { - String[] selector = serializeSelector(((RuleRelativeBlock)rule).getSelector()); - Style decls = new Style(); { - for (Rule r : (RuleRelativeBlock)rule) - if (r instanceof Declaration) - decls.add((Declaration)r); - else if (r instanceof RulePage) - decls.add("@page", Style.of((RulePage)r)); - else - throw new RuntimeException("coding error"); - } - style.add(selector, decls); } - else if (rule instanceof RulePage) - style.add("@page", Style.of((RulePage)rule)); - else if (rule instanceof RuleVolume) - style.add("@volume", Style.of((RuleVolume)rule)); - else if (rule instanceof RuleTextTransform) { - String name = ((RuleTextTransform)rule).getName(); - Style textTransform = new Style().add((List)rule); - if (name == null) - style.add("@text-transform", textTransform); - else - style.add("@text-transform", new Style().add("& " + name, textTransform)); } - else if (rule instanceof RuleHyphenationResource) - style.add("@hyphenation-resource", Style.of((RuleHyphenationResource)rule)); - else if (rule instanceof RuleCounterStyle) { - String name = ((RuleCounterStyle)rule).getName(); - Style counterStyle = new Style().add((List)rule); - style.add("@counter-style", new Style().add("& " + name, counterStyle)); } - else if (rule instanceof RuleMargin) - style.add("@" + ((RuleMargin)rule).getMarginArea(), - new Style().add((List)rule)); - else if (rule instanceof RuleVolumeArea) - style.add("@" + ((RuleVolumeArea)rule).getVolumeArea().value, - Style.of((RuleVolumeArea)rule)); - else if (rule instanceof RuleRelativePage) - style.add(Style.of(((RuleRelativePage)rule).asRulePage())); - else if (rule instanceof RuleRelativeVolume) - style.add(Style.of(((RuleRelativeVolume)rule).asRuleVolume())); - else if (rule instanceof RuleRelativeHyphenationResource) - style.add(Style.of(((RuleRelativeHyphenationResource)rule).asRuleHyphenationResource())); - else if (rule instanceof AnyAtRule) - style.add("@" + ((AnyAtRule)rule).getName(), - Style.of((AnyAtRule)rule)); - else - throw new RuntimeException("coding error"); - } - return style; - } - - @Override - public String toString() { - return BrailleCssSerializer.toString(this); - } - } - - /* Split the selector parts and serialize */ - private static String[] serializeSelector(List combinedSelector) { - List selector = new ArrayList<>(); - for (CombinatorSelectorPart part : flattenSelector(combinedSelector)) - selector.add("&" + part); - return selector.toArray(new String[selector.size()]); - } - - /* Convert a combined selector, which may contain pseudo element parts with other pseudo - * elements stacked onto them, into a flat list of selector parts and combinators */ - private static List flattenSelector(List combinedSelector) { - List selector = new ArrayList<>(); - for (Selector s : combinedSelector) - flattenSelector(selector, s); - return selector; - } - - private static void flattenSelector(List collect, Selector selector) { - Combinator combinator = selector.getCombinator(); - for (SelectorPart part : selector) { - flattenSelector(collect, combinator, part); - combinator = null; - } - } - - private static void flattenSelector(List collect, Combinator combinator, SelectorPart part) { - collect.add(new CombinatorSelectorPart(combinator, part)); - if (part instanceof PseudoElementImpl) { - PseudoElementImpl pe = (PseudoElementImpl)part; - if (!pe.getCombinedSelectors().isEmpty()) - for (Selector s: pe.getCombinedSelectors()) - flattenSelector(collect, s); - else { - if (!pe.getPseudoClasses().isEmpty()) - for (PseudoClass pc : pe.getPseudoClasses()) - collect.add(new CombinatorSelectorPart(null, pc)); - if (pe.hasStackedPseudoElement()) - flattenSelector(collect, null, pe.getStackedPseudoElement()); - } - } - } - - private static class CombinatorSelectorPart { - final Combinator combinator; - final SelectorPart selector; - CombinatorSelectorPart(Combinator combinator, SelectorPart selector) { - this.combinator = combinator; - this.selector = selector; - } - @Override - public String toString() { - StringBuilder b = new StringBuilder(); - if (combinator != null) - b.append(combinator.value()); - if (selector instanceof PseudoElementImpl) { - PseudoElementImpl pe = (PseudoElementImpl)selector; - b.append(":"); - if (!pe.isSpecifiedAsClass()) - b.append(":"); - b.append(pe.getName()); - String[] args = pe.getArguments(); - if (args.length > 0) { - b.append("("); - for (int i = 0; i < args.length; i++) { - if (i > 0) b.append(", "); - b.append(args[i]); - } - b.append(")"); - } - } else - b.append(selector); - return b.toString(); - } - } -} diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/ContentList.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/ContentList.java new file mode 100644 index 0000000000..1ace6dcf58 --- /dev/null +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/ContentList.java @@ -0,0 +1,840 @@ +package org.daisy.pipeline.braille.css.impl; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import cz.vutbr.web.css.Term; +import cz.vutbr.web.css.TermFunction; +import cz.vutbr.web.css.TermIdent; +import cz.vutbr.web.css.TermInteger; +import cz.vutbr.web.css.TermLengthOrPercent; +import cz.vutbr.web.css.TermPercent; +import cz.vutbr.web.css.TermString; +import cz.vutbr.web.css.TermURI; +import cz.vutbr.web.csskit.TermStringImpl; +import cz.vutbr.web.csskit.TermURIImpl; + +import org.daisy.braille.css.BrailleCSSExtension; +import org.daisy.braille.css.BrailleCSSParserFactory.Context; +import org.daisy.braille.css.SupportedBrailleCSS; +import org.daisy.common.file.URLs; + +import org.w3c.dom.Attr; +import org.w3c.dom.Element; + +/** + * This class is immutable + */ +public class ContentList extends AbstractList> implements Term { + + private List> list; + private final BrailleCssParser parser; + private final Context context; + + private ContentList(BrailleCssParser parser, Context context, List> list) { + super(); + this.list = list; + this.parser = parser; + this.context = context; + } + + /** + * @param list assumed to not change + */ + public static ContentList of(BrailleCssParser parser, Context context, List> list) { + return of(parser, null, context, list); + } + + public static ContentList of(BrailleCssParser parser, + Collection extensions, + Context context, + List> list) { + List> items = new ArrayList<>(); + for (Term t : list) { + if (t instanceof UnmodifiableTermString || + t instanceof AttrFunction || + t instanceof ContentFunction || + t instanceof StringFunction || + t instanceof CounterFunction || + t instanceof TextFunction || + t instanceof LeaderFunction || + t instanceof FlowFunction) + items.add(t); + else if (t instanceof TermString) + items.add(new UnmodifiableTermString((TermString)t)); + else if (t instanceof TermFunction) { + TermFunction func = (TermFunction)t; + String funcname = func.getFunctionName(); + if ("attr".equals(funcname)) { + AttrFunction f = AttrFunction.of(func); + if (f != null && !f.asURL) items.add(f); } + else if ("content".equals(funcname) || "target-content".equals(funcname)) { + ContentFunction f = ContentFunction.of(func); + if (f != null) items.add(f); } + else if ("string".equals(funcname) || "target-string".equals(funcname)) { + StringFunction f = StringFunction.of(func); + if (f != null) items.add(f); } + else if ("counter".equals(funcname) || "target-counter".equals(funcname)) { + CounterFunction f = CounterFunction.of(func, extensions); + if (f != null) items.add(f); } + else if ("target-text".equals(funcname)) { + TextFunction f = TextFunction.of(func); + if (f != null) items.add(f); } + else if ("leader".equals(funcname)) { + LeaderFunction f = LeaderFunction.of(func); + if (f != null) items.add(f); } + else if ("flow".equals(funcname)) { + FlowFunction f = FlowFunction.of(func); + if (f != null) items.add(f); } + else if (funcname.matches("-(\\p{L}|_)+-.*")) // vendor prefixed + items.add(func); + } else + throw new IllegalArgumentException("unexpected term in content list: " + t); + } + return new ContentList(parser, context, items); + } + + @Override + public Term get(int index) { + return list.get(index); + } + + @Override + public int size() { + return list.size(); + } + + @Override + public ContentList getValue() { + return this; + } + + @Override + public Operator getOperator() { + return null; + } + + @Override + public ContentList shallowClone() { + try { + return (ContentList)super.clone(); + } catch (CloneNotSupportedException e) { + throw new InternalError("coding error"); + } + } + + @Override + public ContentList clone() { + ContentList clone = shallowClone(); + clone.list = new ArrayList<>(list); + return clone; + } + + @Override + public ContentList setValue(ContentList value) { + throw new UnsupportedOperationException("Unmodifiable"); + } + + @Override + public ContentList setOperator(Operator operator) { + throw new UnsupportedOperationException("Unmodifiable"); + } + + /** + * {@link BrailleCssParser} instance that was used to create this {@link ContentList}. + */ + public BrailleCssParser getParser() { + return parser; + } + + /** + * {@link Context} in which this {@link ContentList} was created. + */ + public Context getContext() { + return context; + } + + /** + * Evaluate attr() and content() values. + * + * This method is mutating, but we can still say that the object is immutable because the method + * is package private and only used by {@link BrailleCssStyle}, {@link StringSetList} and {@link + * BrailleCssParser} _before_ the object is made available. + */ + void evaluate(Element context) { + for (int i = 0; i < list.size(); i++) { + Term t = list.get(i); + if (t instanceof AttrFunction) + list.set(i, ((AttrFunction)t).evaluate(context)); + if (t instanceof ContentFunction) + list.set(i, ((ContentFunction)t).evaluate(context)); + else if (t instanceof StringFunction) + list.set(i, ((StringFunction)t).evaluate(context)); + else if (t instanceof CounterFunction) + list.set(i, ((CounterFunction)t).evaluate(context)); + else if (t instanceof TextFunction) + list.set(i, ((TextFunction)t).evaluate(context)); + if (list.get(i) == null) + list.remove(i--); + } + } + + /** + * This class is immutable + */ + public static class AttrFunction extends UnmodifiableTerm { + + // these fields need to be package private because they are used in BrailleCssSerializer + final TermIdent name; + final boolean asURL; + + private AttrFunction(TermIdent name, boolean asURL) { + this.name = name; + this.asURL = asURL; + } + + /** + * @param fn assumed to not change + */ + private static AttrFunction of(TermFunction fn) { + if (!"attr".equals(fn.getFunctionName())) + return null; + TermIdent name = null; + boolean asURL = false; + for (Term arg : fn) + if (name == null) + if (arg instanceof TermIdent) + name = (TermIdent)arg; + else + return null; + else if (asURL == false) + if (arg instanceof TermIdent) + if ("url".equals(((TermIdent)arg).getValue())) + asURL = true; + else + return null; + else + return null; + else + return null; + if (name != null) + return new AttrFunction(name, asURL); + return null; + } + + /** + * Evaluate attr() and content() values. + */ + private Term evaluate(Element context) { + Attr attr = context.getAttributeNode(name.getValue()); + if (attr == null) + return null; + if (asURL) { + TermURI evaluated = new TermURIImpl(){}.setValue(attr.getValue()); + String base = context.getBaseURI(); + if (base != null && !"".equals(base)) + evaluated.setBase(URLs.asURL(base)); + return new UnmodifiableTermURI(evaluated); + } else + return new UnmodifiableTermString(new TermStringImpl(){}.setValue(attr.getValue())); + } + } + + public static class URL { + + // these fields need to be package private because they are used in BrailleCssSerializer + final TermURI url; + final AttrFunction urlAttr; + + private URL(TermURI url) { + this.url = url; + this.urlAttr = null; + } + + private URL(AttrFunction urlAttr) { + this.url = null; + this.urlAttr = urlAttr; + } + + /** + * @param t assumed to not change + */ + private static URL of(Term t) { + if (t instanceof TermURI) + return new URL((TermURI)t); + else if (t instanceof TermFunction) { + AttrFunction a = AttrFunction.of((TermFunction)t); + if (a != null) { + if (!a.asURL) + a = new AttrFunction(a.name, true); + return new URL(a); + } else + return null; } + else + return null; + } + + /** + * Evaluate attr() and content() values. + */ + private URL evaluate(Element context) { + if (urlAttr != null) { + Attr attr = context.getAttributeNode(urlAttr.name.getValue()); + if (attr == null) + return null; + TermURI evaluated = new TermURIImpl(){}.setValue(attr.getValue()); + String base = context.getBaseURI(); + if (base != null && !"".equals(base)) + evaluated.setBase(URLs.asURL(base)); + return new URL(evaluated); + } else + return this; + } + } + + /** + * This class is immutable + */ + public static class ContentFunction extends UnmodifiableTerm { + + // this field needs to be package private because it is used in BrailleCssSerializer + final Optional target; + + private ContentFunction() { + this.target = Optional.empty(); + } + + private ContentFunction(URL target) { + this.target = Optional.of(target); + } + + /** + * @param fn assumed to not change + */ + private static ContentFunction of(TermFunction fn) { + boolean needTarget = false; + if ("target-content".equals(fn.getFunctionName())) + needTarget = true; + else if (!"content".equals(fn.getFunctionName())) + return null; + URL target = null; + for (Term arg : fn) + if (needTarget && target == null) { + target = URL.of(arg); + if (target == null) + return null; } + else + return null; + if (needTarget && target != null) + return new ContentFunction(target); + else if (!needTarget && target == null) + return new ContentFunction(); + return null; + } + + /** + * Evaluate attr() and content() values. + */ + private Term evaluate(Element context) { + if (target.isPresent()) { + URL url = target.get(); + URL evaluatedURL = url.evaluate(context); + if (evaluatedURL == null) + return null; + else if (evaluatedURL != url) + return new ContentFunction(evaluatedURL); + } else + return new UnmodifiableTermString(new TermStringImpl(){}.setValue(context.getTextContent())); + return this; + } + } + + /** + * This class is immutable + */ + public static class StringFunction extends UnmodifiableTerm { + + // these fields need to be package private because they are used in BrailleCssSerializer + final Optional target; + final TermIdent name; + final Optional scope; + + public enum Scope { + + FIRST("first"), + START("start"), + LAST("last"), + START_EXCEPT_FIRST("start-except-first"), + START_EXCEPT_LAST("start-except-last"), + LAST_EXCEPT_START("last-except-start"), + PAGE_FIRST("page-first"), + PAGE_START("page-start"), + PAGE_LAST("page-last"), + PAGE_START_EXCEPT_LAST("page-start-except-last"), + PAGE_LAST_EXCEPT_START("page-last-except-start"), + SPREAD_FIRST("spread-first"), + SPREAD_START("spread-start"), + SPREAD_LAST("spread-last"), + SPREAD_START_EXCEPT_LAST("spread-start-except-last"), + SPREAD_LAST_EXCEPT_START("spread-last-except-start"); + + private final String ident; + + private static final Map lookup = new HashMap<>(); static { + for (Scope scope : EnumSet.allOf(Scope.class)) + lookup.put(scope.ident, scope); } + + private Scope(String ident) { + this.ident = ident; + } + + public static Scope of(TermIdent term) throws IllegalArgumentException { + Scope scope = lookup.get(term.getValue()); + if (scope == null) + throw new IllegalArgumentException("unexpected scope argument in string() function: " + term.getValue()); + return scope; + } + + @Override + public String toString() { + return ident; + } + } + + private StringFunction(TermIdent name, Optional scope) throws IllegalArgumentException { + this.target = Optional.empty(); + this.name = name; + this.scope = scope.map(Scope::of); + } + + private StringFunction(URL target, TermIdent name) { + this.target = Optional.of(target); + this.name = name; + this.scope = Optional.empty(); + } + + /** + * @param fn assumed to not change + */ + private static StringFunction of(TermFunction fn) { + boolean needTarget = false; + if ("target-string".equals(fn.getFunctionName())) + needTarget = true; + else if (!"string".equals(fn.getFunctionName())) + return null; + URL target = null; + TermIdent name = null; + TermIdent scope = null; + for (Term arg : fn) + if (needTarget && target == null) { + target = URL.of(arg); + if (target == null) + return null; } + else if (name == null) + if (arg instanceof TermIdent) + name = (TermIdent)arg; + else + return null; + else if (scope == null) + if (arg instanceof TermIdent) + scope = (TermIdent)arg; + else + return null; + else + return null; + if (name != null) + if (needTarget && target != null) + if (scope != null) + return null; + else + return new StringFunction(target, name); + else if (!needTarget && target == null) + try { + return new StringFunction(name, Optional.ofNullable(scope)); + } catch (IllegalArgumentException e) { + // invalid scope argument + } + return null; + } + + /** + * Evaluate attr() and content() values. + */ + private StringFunction evaluate(Element context) { + if (target.isPresent()) { + URL url = target.get(); + URL evaluatedURL = url.evaluate(context); + if (evaluatedURL == null) + return null; + else if (evaluatedURL != url) + return new StringFunction(evaluatedURL, name); + } + return this; + } + } + + public static class CounterStyle { + + // these fields need to be package private because they are used in BrailleCssSerializer + final String name; + final TermString symbol; + final TermFunction symbols; + final org.daisy.pipeline.css.CounterStyle style; + + private CounterStyle(TermIdent name) { + if ("none".equals(name.getValue())) + this.name = null; + else + this.name = name.getValue(); + this.symbol = null; + this.symbols = null; + this.style = null; + } + + private CounterStyle(TermString symbol) { + this.name = null; + this.symbol = symbol; + this.symbols = null; + this.style = org.daisy.pipeline.css.CounterStyle.constant(symbol.getValue()); + } + + private CounterStyle(TermFunction style) throws IllegalArgumentException { + this.name = null; + this.symbol = null; + this.symbols = style; + this.style = org.daisy.pipeline.css.CounterStyle.fromSymbolsFunction(style); + } + + /** + * @param t assumed to not change + */ + private static CounterStyle of(Term t) { + if (t instanceof TermIdent) + return new CounterStyle((TermIdent)t); + else if (t instanceof TermString) + return new CounterStyle((TermString)t); + else if (t instanceof TermFunction) + try { + return new CounterStyle((TermFunction)t); + } catch (IllegalArgumentException e) { + // invalid symbols() function + } + return null; + } + } + + /** + * This class is immutable + */ + public static class CounterFunction extends UnmodifiableTerm + implements org.daisy.pipeline.braille.css.CounterFunction { + + // these fields need to be package private because they are used in BrailleCssSerializer + final Optional target; + final TermIdent name; + final Optional style; + + @Override + public String getCounter() { + return name.getValue(); + } + + @Override + public Optional getStyle() { + return style.map(x -> x.style); + } + + private CounterFunction(TermIdent name, Optional style) { + this.target = Optional.empty(); + this.name = name; + this.style = style; + } + + private CounterFunction(URL target, TermIdent name, Optional style) { + this.target = Optional.of(target); + this.name = name; + this.style = style; + } + + /** + * @param fn assumed to not change + */ + private static CounterFunction of(TermFunction fn, Collection extensions) { + boolean needTarget = false; + if ("target-counter".equals(fn.getFunctionName())) + needTarget = true; + else if (!"counter".equals(fn.getFunctionName())) + return null; + URL target = null; + TermIdent name = null; + CounterStyle style = null; + for (Term arg : fn) + if (needTarget && target == null) { + target = URL.of(arg); + if (target == null) + return null; } + else if (name == null) { + if (extensions != null) + if (arg instanceof TermIdent && ((TermIdent)arg).getValue().startsWith("-")) { + // if the counter name starts with an extension prefix, let the extension parse it + String n = ((TermIdent)arg).getValue(); + for (BrailleCSSExtension x : extensions) + if (n.startsWith(x.getPrefix())) + try { + name = x.parseCounterName(arg); + break; + } catch (IllegalArgumentException e) { + return null; + } + } else + // other, give extensions the chance to normalize names + for (BrailleCSSExtension x : extensions) + try { + name = x.parseCounterName(arg); + break; + } catch (IllegalArgumentException e) { + // continue + } + if (name == null && arg instanceof TermIdent) + name = (TermIdent)arg; + if (name == null) + return null; + } else if (style == null) { + style = CounterStyle.of(arg); + if (style == null) + return null; } + else + return null; + if (name != null) + if (needTarget && target != null) + return new CounterFunction(target, name, Optional.ofNullable(style)); + else if (!needTarget && target == null) + return new CounterFunction(name, Optional.ofNullable(style)); + return null; + } + + /** + * Evaluate attr() and content() values. + */ + private CounterFunction evaluate(Element context) { + if (target.isPresent()) { + URL url = target.get(); + URL evaluatedURL = url.evaluate(context); + if (evaluatedURL == null) + return null; + else if (evaluatedURL != url) + return new CounterFunction(evaluatedURL, name, style); + } + return this; + } + } + + /** + * This class is immutable + */ + public static class TextFunction extends UnmodifiableTerm { + + // this field needs to be package private because it is used in BrailleCssSerializer + final URL target; + + private TextFunction(URL target) { + this.target = target; + } + + /** + * @param fn assumed to not change + */ + private static TextFunction of(TermFunction fn) { + if (!"target-text".equals(fn.getFunctionName())) + return null; + URL target = null; + for (Term arg : fn) + if (target == null) { + target = URL.of(arg); + if (target == null) + return null; } + else + return null; + if (target != null) + return new TextFunction(target); + return null; + } + + /** + * Evaluate attr() and content() values. + */ + private TextFunction evaluate(Element context) { + URL evaluatedTarget = target.evaluate(context); + if (evaluatedTarget == null) + return null; + else if (evaluatedTarget != target) + return new TextFunction(evaluatedTarget); + return this; + } + } + + /** + * This class is immutable + */ + public static class LeaderFunction extends UnmodifiableTerm { + + // these fields need to be package private because they are used in BrailleCssSerializer + final TermString pattern; + final Optional position; + final Optional alignment; + + public enum Alignment { + + LEFT("left"), + CENTER("center"), + RIGHT("right"); + + private final String ident; + + private static final Map lookup = new HashMap<>(); static { + for (Alignment alignment : EnumSet.allOf(Alignment.class)) + lookup.put(alignment.ident, alignment); } + + private Alignment(String ident) { + this.ident = ident; + } + + public static Alignment of(TermIdent term) throws IllegalArgumentException { + Alignment alignment = lookup.get(term.getValue()); + if (alignment == null) + throw new IllegalArgumentException("unexpected alignment argument in leader() function: " + term.getValue()); + return alignment; + } + + @Override + public String toString() { + return ident; + } + } + + private LeaderFunction(TermString pattern, Optional position, Optional alignment) + throws IllegalArgumentException { + this.pattern = pattern; + this.position = position; + this.alignment = alignment.map(Alignment::of); + } + + /** + * @param fn assumed to not change + */ + private static LeaderFunction of(TermFunction fn) { + if (!"leader".equals(fn.getFunctionName())) + return null; + TermString pattern = null; + TermLengthOrPercent position = null; + TermIdent alignment = null; + for (Term arg : fn) + if (pattern == null) + if (arg instanceof TermString) { + pattern = (TermString)arg; + if (!pattern.getValue().matches("[\\u2800-\\u28ff]*")) // \\p{IsBraillePatterns}* + return null; } + else + return null; + else if (position == null) + if (arg instanceof TermInteger || arg instanceof TermPercent) + position = (TermLengthOrPercent)arg; + else + return null; + else if (alignment == null) + if (arg instanceof TermIdent) + alignment = (TermIdent)arg; + else + return null; + else + return null; + if (pattern != null) + try { + return new LeaderFunction(pattern, Optional.ofNullable(position), Optional.ofNullable(alignment)); + } catch (IllegalArgumentException e) { + // invalid alignment argument + } + return null; + } + } + + /** + * This class is immutable + */ + public static class FlowFunction extends UnmodifiableTerm { + + // these fields need to be package private because they are used in BrailleCssSerializer + final TermIdent from; + final Optional scope; + + public enum Scope { + + DOCUMENT("document"), + VOLUME("volume"), + PAGE("page"); + + private final String ident; + + private static final Map lookup = new HashMap<>(); static { + for (Scope scope : EnumSet.allOf(Scope.class)) + lookup.put(scope.ident, scope); } + + private Scope(String ident) { + this.ident = ident; + } + + public static Scope of(TermIdent term) throws IllegalArgumentException { + Scope scope = lookup.get(term.getValue()); + if (scope == null) + throw new IllegalArgumentException("unexpected scope argument in string() function: " + term.getValue()); + return scope; + } + + @Override + public String toString() { + return ident; + } + } + + private FlowFunction(TermIdent from, Optional scope) throws IllegalArgumentException { + this.from = from; + this.scope = scope.map(Scope::of); + } + + /** + * @param fn assumed to not change + */ + private static FlowFunction of(TermFunction fn) { + if (!"flow".equals(fn.getFunctionName())) + return null; + TermIdent from = null; + TermIdent scope = null; + for (Term arg : fn) + if (from == null) + if (arg instanceof TermIdent) + from = (TermIdent)arg; + else + return null; + else if (scope == null) + if (arg instanceof TermIdent) + scope = (TermIdent)arg; + else + return null; + else + return null; + if (from != null) + try { + return new FlowFunction(from, Optional.ofNullable(scope)); + } catch (IllegalArgumentException e) { + // invalid scope argument + } + return null; + } + } +} diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/CounterSetList.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/CounterSetList.java new file mode 100644 index 0000000000..dbd4cb2fa2 --- /dev/null +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/CounterSetList.java @@ -0,0 +1,116 @@ +package org.daisy.pipeline.braille.css.impl; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import cz.vutbr.web.css.Term; +import cz.vutbr.web.css.TermPair; + +/** + * This class is immutable + */ +public class CounterSetList extends AbstractList> implements Term { + + /** + * This class is immutable + */ + public static class CounterSet extends UnmodifiableTerm implements org.daisy.pipeline.braille.css.CounterSet { + private final String name; + private final Integer value; + private CounterSet(String name, Integer value) { + super(); + this.name = name; + this.value = value; + } + @Override + public String getKey() { + return name; + } + @Override + public Integer getValue() { + return value; + } + @Override + public TermPair setKey(String key) { + throw new UnsupportedOperationException("Unmodifiable"); + } + } + + private List> list; + + private CounterSetList(List> list) { + super(); + this.list = list; + } + + /** + * @param list assumed to not change + */ + public static CounterSetList of(List> list) { + Map pairs = new LinkedHashMap<>(); // preserves order of insertion + for (Term t : list) { + if (t instanceof TermPair) { + TermPair pair = (TermPair)t; + Object k = pair.getKey(); + Object v = pair.getValue(); + if (k instanceof String && v instanceof Integer) { + pairs.remove((String)k); + pairs.put((String)k, (Integer)v); + } else + throw new IllegalArgumentException("unexpected term in counter-set list: " + t); + } else + throw new IllegalArgumentException("unexpected term in counter-set list: " + t); + } + List> pairsList = new ArrayList(); + for (Map.Entry e : pairs.entrySet()) + pairsList.add(new CounterSet(e.getKey(), e.getValue())); + return new CounterSetList(pairsList); + } + + @Override + public Term get(int index) { + return list.get(index); + } + + @Override + public int size() { + return list.size(); + } + + @Override + public CounterSetList getValue() { + return this; + } + + @Override + public Operator getOperator() { + return null; + } + + @Override + public CounterSetList shallowClone() { + try { + return (CounterSetList)super.clone(); + } catch (CloneNotSupportedException e) { + throw new InternalError("coding error"); + } + } + + @Override + public CounterSetList clone() { + return shallowClone(); + } + + @Override + public CounterSetList setValue(CounterSetList value) { + throw new UnsupportedOperationException("Unmodifiable"); + } + + @Override + public CounterSetList setOperator(Operator operator) { + throw new UnsupportedOperationException("Unmodifiable"); + } +} diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/StringSetList.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/StringSetList.java new file mode 100644 index 0000000000..6d1358d4af --- /dev/null +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/StringSetList.java @@ -0,0 +1,160 @@ +package org.daisy.pipeline.braille.css.impl; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.List; + +import cz.vutbr.web.css.Term; +import cz.vutbr.web.css.TermList; +import cz.vutbr.web.css.TermPair; +import cz.vutbr.web.css.TermString; + +import org.daisy.braille.css.BrailleCSSParserFactory.Context; +import org.daisy.pipeline.braille.css.impl.ContentList.AttrFunction; +import org.daisy.pipeline.braille.css.impl.ContentList.ContentFunction; + +import org.w3c.dom.Element; + +/** + * This class is immutable + */ +public class StringSetList extends AbstractList> implements Term { + + /** + * This class is immutable + */ + public static class StringSet extends UnmodifiableTerm implements TermPair { + private final String name; + private final ContentList value; + private final Operator operator; + private StringSet(String name, ContentList value, Operator operator) throws IllegalArgumentException { + this(name, value, operator, false); + } + private StringSet(String name, ContentList value, Operator operator, boolean noCheck) throws IllegalArgumentException { + super(); + if (!noCheck) { + for (Term v : value) + if (!(v instanceof TermString + || v instanceof AttrFunction + || (v instanceof ContentFunction && !((ContentFunction)v).target.isPresent()))) + throw new IllegalArgumentException("unexpected term in string-set list: " + v); + } + this.name = name; + this.value = value; + this.operator = operator; + } + @Override + public String getKey() { + return name; + } + @Override + public ContentList getValue() { + return value; + } + @Override + public Operator getOperator() { + return operator; + } + @Override + public TermPair setKey(String key) { + throw new UnsupportedOperationException("Unmodifiable"); + } + @Override + public String toString() { + return BrailleCssSerializer.toString(this); + } + /** + * Evaluate attr() and content() values. + */ + private StringSet evaluate(Element context) { + ContentList evaluatedValue = value.clone(); + evaluatedValue.evaluate(context); + return new StringSet(name, evaluatedValue, operator, true); + } + } + + private List> list; + + private StringSetList(List> list) { + super(); + this.list = list; + } + + /** + * @param list assumed to not change + */ + public static StringSetList of(BrailleCssParser parser, Context context, TermList list) throws IllegalArgumentException { + List> pairs = new ArrayList<>(); + for (Term t : list) { + if (t instanceof TermPair) { + TermPair pair = (TermPair)t; + Object k = pair.getKey(); + Object v = pair.getValue(); + if (k instanceof String && v instanceof TermList) + pairs.add(new StringSet((String)k, ContentList.of(parser, context, (TermList)v), t.getOperator())); + else + throw new IllegalArgumentException("unexpected term in string-set list: " + t); + } else + throw new IllegalArgumentException("unexpected term in string-set list: " + t); + } + return new StringSetList(pairs); + } + + @Override + public Term get(int index) { + return list.get(index); + } + + @Override + public int size() { + return list.size(); + } + + @Override + public StringSetList getValue() { + return this; + } + + @Override + public Operator getOperator() { + return null; + } + + @Override + public StringSetList shallowClone() { + try { + return (StringSetList)super.clone(); + } catch (CloneNotSupportedException e) { + throw new InternalError("coding error"); + } + } + + @Override + public StringSetList clone() { + StringSetList clone = shallowClone(); + clone.list = new ArrayList<>(list); + return clone; + } + + @Override + public StringSetList setValue(StringSetList value) { + throw new UnsupportedOperationException("Unmodifiable"); + } + + @Override + public StringSetList setOperator(Operator operator) { + throw new UnsupportedOperationException("Unmodifiable"); + } + + /** + * Evaluate attr() and content() values. + * + * This method is mutating, but we can still say that the object is immutable because the method + * is package private and only used by {@link BrailleCssStyle} and {@link BrailleCssParser} + * _before_ the object is made available. + */ + void evaluate(Element context) { + for (int i = 0; i < list.size(); i++) + list.set(i, ((StringSet)list.get(i)).evaluate(context)); + } +} diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/StyleTransformerImpl.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/StyleTransformerImpl.java new file mode 100644 index 0000000000..1ca4d3e8ec --- /dev/null +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/StyleTransformerImpl.java @@ -0,0 +1,129 @@ +package org.daisy.pipeline.braille.css.impl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import cz.vutbr.web.css.CSSProperty; +import cz.vutbr.web.css.Declaration; +import cz.vutbr.web.css.Term; + +import org.daisy.braille.css.BrailleCSSParserFactory; +import org.daisy.braille.css.BrailleCSSParserFactory.Context; +import org.daisy.braille.css.PropertyValue; +import org.daisy.braille.css.SimpleInlineStyle; +import org.daisy.braille.css.SupportedBrailleCSS; +import org.daisy.pipeline.braille.css.impl.BrailleCssParser.ParsedDeclaration; +import org.daisy.pipeline.braille.css.impl.BrailleCssParser.ParsedDeclarations; +import org.daisy.pipeline.braille.css.StyleTransformer; +import org.daisy.pipeline.braille.css.xpath.Style; +import org.daisy.pipeline.braille.css.xpath.impl.Stylesheet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class StyleTransformerImpl implements StyleTransformer { + + private final static Logger logger = LoggerFactory.getLogger(StyleTransformer.class); + + private final SupportedBrailleCSS fromSupportedBrailleCSS; + private final SupportedBrailleCSS toSupportedBrailleCSS; + private final BrailleCssParser parser; + private final Set propertiesWithDifferentDefault; + + public StyleTransformerImpl(SupportedBrailleCSS fromSupportedBrailleCSS, + SupportedBrailleCSS toSupportedBrailleCSS) { + this.fromSupportedBrailleCSS = fromSupportedBrailleCSS; + this.toSupportedBrailleCSS = toSupportedBrailleCSS; + this.parser = new BrailleCssParser() { + @Override + public BrailleCSSParserFactory getBrailleCSSParserFactory() { + return null; }; // should be fine to return null (assuming parseInlineStyle() will not + // be called on this parser object) + @Override + public Optional getSupportedBrailleCSS(Context context) { + return Optional.of(toSupportedBrailleCSS); }}; + this.propertiesWithDifferentDefault = new HashSet<>(); { + for (String p : toSupportedBrailleCSS.getDefinedPropertyNames()) + if (fromSupportedBrailleCSS.isSupportedCSSProperty(p)) + if ("".equals(fromSupportedBrailleCSS.getDefaultProperty(p).toString()) + ? (Objects.equals(fromSupportedBrailleCSS.getDefaultProperty(p), + toSupportedBrailleCSS.getDefaultProperty(p)) && + Objects.equals(fromSupportedBrailleCSS.getDefaultValue(p), + toSupportedBrailleCSS.getDefaultValue(p))) + : Objects.equals(fromSupportedBrailleCSS.getDefaultProperty(p).toString(), + toSupportedBrailleCSS.getDefaultProperty(p).toString())) + ; // same default + else + propertiesWithDifferentDefault.add(p); + } + } + + @Override + public final SimpleInlineStyle transform(SimpleInlineStyle style) { + Map properties = new HashMap<>(); + Map> values = new HashMap<>(); + Map sourceDeclarations = new HashMap<>(); + List declarations = new ArrayList<>(); + for (PropertyValue d : style) { + if (d.getSupportedBrailleCSS() != fromSupportedBrailleCSS) + throw new IllegalArgumentException(); + String p = d.getProperty(); + if (!toSupportedBrailleCSS.isSupportedCSSProperty(p)) { + // ignore "inherit" (if style is based on parent, inherit has already been concretized; + // if not based on parent, this is equivalent to "initial") + if (d.getCSSProperty().equalsInherit()) + ; + // ignore "initial" (should already have been concretized; properties with different + // default in the output model are handled below) + else if (d.getCSSProperty().equalsInitial()) + ; + // ignore values that are the default (equivalent to "initial") + else if (Objects.equals(d.getCSSProperty(), fromSupportedBrailleCSS.getDefaultProperty(p)) && + Objects.equals(d.getValue(), fromSupportedBrailleCSS.getDefaultValue(p))) + ; + else + logger.warn("Property '{}' not supported", p); + } else if (!toSupportedBrailleCSS.parseDeclaration(d, properties, values)) + logger.warn("Property '{}' not supported", d); + else + sourceDeclarations.put(p, d.getSourceDeclaration()); + } + for (String p : toSupportedBrailleCSS.getDefinedPropertyNames()) { + CSSProperty property = null; + Term value = null; + if (properties.containsKey(p)) + declarations.add(new ParsedDeclaration(parser, + Context.ELEMENT, + p, + properties.get(p), + values.get(p), + sourceDeclarations.get(p))); + else if (propertiesWithDifferentDefault.contains(p)) + declarations.add(new ParsedDeclaration(parser, + Context.ELEMENT, + p, + toSupportedBrailleCSS.getDefaultProperty(p), + toSupportedBrailleCSS.getDefaultValue(p), + null)); + } + return new ParsedDeclarations(parser, Context.ELEMENT, declarations); + } + + @Override + public final Style transform(Style style) { + if (!(style instanceof Stylesheet)) + throw new IllegalArgumentException(); + Stylesheet fs = (Stylesheet)style; + SimpleInlineStyle s = fs.style != null + ? fs.style.asSimpleInlineStyle(false) + : SimpleInlineStyle.EMPTY; + s = transform(s); + return Stylesheet.of(BrailleCssStyle.of(s)); + } +} diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/TextTransformList.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/TextTransformList.java new file mode 100644 index 0000000000..14576e4216 --- /dev/null +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/TextTransformList.java @@ -0,0 +1,181 @@ +package org.daisy.pipeline.braille.css.impl; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.List; + +import org.daisy.braille.css.BrailleCSSProperty.TextTransform; +import org.daisy.braille.css.PropertyValue; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +import cz.vutbr.web.css.CSSProperty; +import cz.vutbr.web.css.Term; +import cz.vutbr.web.css.TermIdent; +import cz.vutbr.web.css.TermList; +import cz.vutbr.web.csskit.TermIdentImpl; + +/** + * This class is immutable except when locked is set to false in which case terms may be removed. + */ +public class TextTransformList extends AbstractList> implements TermList { + + private List list; + + // object should always be cloned before it is unlocked + boolean locked = true; + + private final static ImmutableList INITIAL = ImmutableList.of(new UnmodifiableTermIdent(new TermIdentImpl(){}.setValue("initial"))); + private final static ImmutableList AUTO = ImmutableList.of(new UnmodifiableTermIdent(new TermIdentImpl(){}.setValue("auto"))); + private final static ImmutableList NONE = ImmutableList.of(new UnmodifiableTermIdent(new TermIdentImpl(){}.setValue("none"))); + + private TextTransformList(List list) { + super(); + this.list = list; + } + + /** + * @param value assumed to not change + */ + public static TextTransformList of(TermIdent value) { + String v = value.getValue(); + if (v.equalsIgnoreCase("initial")) + return new TextTransformList(INITIAL); + else if (v.equalsIgnoreCase("auto")) + return new TextTransformList(AUTO); + else if (v.equalsIgnoreCase("none")) + return new TextTransformList(NONE); + else if (v.equalsIgnoreCase("inherit")) + throw new IllegalArgumentException(); + else { + List list = new ArrayList<>(); + list.add(new UnmodifiableTermIdent(value)); + return new TextTransformList(list); + } + } + + /** + * @param value assumed to not change + */ + public static TextTransformList of(TermList value) { + List list = new ArrayList<>(); + for (Term t : value) { + if (t instanceof TermIdent) + list.add(new UnmodifiableTermIdent((TermIdent)t)); + else + throw new IllegalArgumentException("unexpected term in text-transform list: " + t); + } + return new TextTransformList(list); + } + + /** + * @param value assumed to not change + */ + public static TextTransformList of(PropertyValue value) { + CSSProperty p = value.getCSSProperty(); + if (p instanceof TextTransform) { + if (p == TextTransform.list_values) { + Term v = value.getValue(); + if (v instanceof TextTransformList) + return (TextTransformList)v; + else if (v instanceof TermList) + return of((TermList)v); + else + throw new IllegalStateException(); // should not happen + } else if (p == TextTransform.AUTO) + return new TextTransformList(AUTO); + else if (p == TextTransform.INITIAL) + return new TextTransformList(INITIAL); + else if (p == TextTransform.NONE) + return new TextTransformList(NONE); + } + throw new IllegalArgumentException(); + } + + @Override + public TermIdent get(int index) { + return list.get(index); + } + + @Override + public TermIdent remove(int index) { + if (locked) + throw new UnsupportedOperationException("Unmodifiable"); + return list.remove(index); + } + + @Override + public int size() { + return list.size(); + } + + @Override + public TextTransformList getValue() { + return this; + } + + @Override + public Operator getOperator() { + return null; + } + + @Override + public TextTransformList shallowClone() { + try { + return (TextTransformList)super.clone(); + } catch (CloneNotSupportedException e) { + throw new InternalError("coding error"); + } + } + + @Override + public TextTransformList clone() { + TextTransformList clone = shallowClone(); + if (!(list instanceof ImmutableList)) + clone.list = new ArrayList<>(list); + return clone; + } + + @Override + public TextTransformList setValue(List> value) { + throw new UnsupportedOperationException("Unmodifiable"); + } + + @Override + public TextTransformList setOperator(Operator operator) { + throw new UnsupportedOperationException("Unmodifiable"); + } + + /** + * This method is mutating, but we can still say that the object is immutable because the method + * is package private and only used by {@link BrailleCssStyle} _before_ the object is made + * available. + */ + void inheritFrom(TextTransformList parent) { + if (equalsNone() || parent.equalsAuto() || parent.equalsInitial() || parent.equalsNone()) + ; + else if (equalsAuto() || equalsInitial()) + list = parent.list; + else { + List list = new ArrayList<>(); + list.addAll(this.list); + for (TermIdent t : parent.list) + if (!Iterables.any(list, x -> x.getValue().equalsIgnoreCase(t.getValue()))) + list.add(t); + this.list = list; + } + } + + public boolean equalsAuto() { + return list == AUTO; + } + + public boolean equalsInitial() { + return list == INITIAL; + } + + public boolean equalsNone() { + return list == NONE; + } +} diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/UnmodifiableTerm.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/UnmodifiableTerm.java new file mode 100644 index 0000000000..5bca393948 --- /dev/null +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/UnmodifiableTerm.java @@ -0,0 +1,41 @@ +package org.daisy.pipeline.braille.css.impl; + +import cz.vutbr.web.css.Term; + +abstract class UnmodifiableTerm implements Term { + + @Override + public T getValue() { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public Operator getOperator() { + return null; + } + + @SuppressWarnings("unchecked") + @Override + public UnmodifiableTerm shallowClone() { + try { + return (UnmodifiableTerm)super.clone(); + } catch (CloneNotSupportedException e) { + throw new InternalError("coding error"); + } + } + + @Override + public UnmodifiableTerm clone() { + return shallowClone(); + } + + @Override + public UnmodifiableTerm setValue(T value) { + throw new UnsupportedOperationException("Unmodifiable"); + } + + @Override + public UnmodifiableTerm setOperator(Operator operator) { + throw new UnsupportedOperationException("Unmodifiable"); + } +} diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/UnmodifiableTermIdent.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/UnmodifiableTermIdent.java new file mode 100644 index 0000000000..ae002b7815 --- /dev/null +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/UnmodifiableTermIdent.java @@ -0,0 +1,18 @@ +package org.daisy.pipeline.braille.css.impl; + +import cz.vutbr.web.css.Term; +import cz.vutbr.web.css.TermIdent; + +class UnmodifiableTermIdent extends UnmodifiableTerm implements TermIdent { + + private final Term ident; + + UnmodifiableTermIdent(Term ident) { + this.ident = ident; + } + + @Override + public String getValue() { + return ident.getValue(); + } +} diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/UnmodifiableTermString.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/UnmodifiableTermString.java new file mode 100644 index 0000000000..f1471eefba --- /dev/null +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/UnmodifiableTermString.java @@ -0,0 +1,18 @@ +package org.daisy.pipeline.braille.css.impl; + +import cz.vutbr.web.css.Term; +import cz.vutbr.web.css.TermString; + +class UnmodifiableTermString extends UnmodifiableTerm implements TermString { + + private final Term string; + + UnmodifiableTermString(Term string) { + this.string = string; + } + + @Override + public String getValue() { + return string.getValue(); + } +} \ No newline at end of file diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/UnmodifiableTermURI.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/UnmodifiableTermURI.java new file mode 100644 index 0000000000..30cf8fdd94 --- /dev/null +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/impl/UnmodifiableTermURI.java @@ -0,0 +1,25 @@ +package org.daisy.pipeline.braille.css.impl; + +import cz.vutbr.web.css.TermURI; + +class UnmodifiableTermURI extends UnmodifiableTerm implements TermURI { + + private final TermURI uri; + + UnmodifiableTermURI(TermURI uri) { + this.uri = uri; + } + + @Override + public String getValue() { + return uri.getValue(); + } + + public java.net.URL getBase() { + return uri.getBase(); + } + + public TermURI setBase(java.net.URL base) { + throw new UnsupportedOperationException("Unmodifiable"); + } +} \ No newline at end of file diff --git a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/saxon/impl/ParseStylesheetDefinition.java b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/saxon/impl/ParseStylesheetDefinition.java index 9a7a1f31e8..d7572e62a4 100644 --- a/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/saxon/impl/ParseStylesheetDefinition.java +++ b/modules/braille/braille-css-utils/src/main/java/org/daisy/pipeline/braille/css/saxon/impl/ParseStylesheetDefinition.java @@ -1,126 +1,103 @@ package org.daisy.pipeline.braille.css.saxon.impl; -import java.util.ArrayList; -import java.util.List; - -import javax.xml.namespace.QName; - -import net.sf.saxon.expr.XPathContext; -import net.sf.saxon.lib.ExtensionFunctionCall; -import net.sf.saxon.lib.ExtensionFunctionDefinition; -import net.sf.saxon.om.Item; -import net.sf.saxon.om.NodeInfo; -import net.sf.saxon.om.Sequence; -import net.sf.saxon.om.StructuredQName; -import net.sf.saxon.s9api.XdmNode; -import net.sf.saxon.trans.XPathException; -import net.sf.saxon.value.BooleanValue; -import net.sf.saxon.value.EmptySequence; -import net.sf.saxon.value.QNameValue; -import net.sf.saxon.value.SequenceExtent; -import net.sf.saxon.value.SequenceType; +import java.util.Optional; import org.daisy.braille.css.BrailleCSSParserFactory.Context; -import org.daisy.braille.css.InlineStyle; -import org.daisy.common.saxon.SaxonOutputValue; -import org.daisy.pipeline.braille.css.impl.BrailleCssSerializer; -import org.daisy.pipeline.braille.css.impl.BrailleCssTreeBuilder.Style; +import org.daisy.pipeline.braille.css.impl.BrailleCssParser; +import org.daisy.pipeline.braille.css.impl.BrailleCssStyle; +import org.daisy.pipeline.braille.css.xpath.impl.Declaration; +import org.daisy.pipeline.braille.css.xpath.impl.Stylesheet; +import org.daisy.pipeline.braille.css.xpath.Style; + +import org.daisy.common.xpath.saxon.ExtensionFunctionProvider; +import org.daisy.common.xpath.saxon.ReflexiveExtensionFunctionProvider; import org.osgi.service.component.annotations.Component; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.w3c.dom.Attr; +import org.w3c.dom.Element; +import org.w3c.dom.Node; @Component( - name = "css:parse-stylesheet", - service = { ExtensionFunctionDefinition.class } + name = "ParseStylesheet", + service = { ExtensionFunctionProvider.class } ) -public class ParseStylesheetDefinition extends ExtensionFunctionDefinition { - +public class ParseStylesheetDefinition extends ReflexiveExtensionFunctionProvider { + private static final String XMLNS_CSS = "http://www.daisy.org/ns/pipeline/braille-css"; - - private static final StructuredQName funcname = new StructuredQName("css", XMLNS_CSS, "parse-stylesheet"); - - private static final QName PAGE = new QName("page"); - private static final QName VOLUME = new QName("volume"); - private static final QName HYPHENATION_RESOURCE = new QName("hyphenation-resource"); - private static final QName VENDOR_RULE = new QName("vendor-rule"); - - public StructuredQName getFunctionQName() { - return funcname; - } - - @Override - public int getMinimumNumberOfArguments() { - return 1; - } - - @Override - public int getMaximumNumberOfArguments() { - return 3; - } - - public SequenceType[] getArgumentTypes() { - return new SequenceType[] { - SequenceType.OPTIONAL_STRING, - SequenceType.SINGLE_BOOLEAN, - SequenceType.OPTIONAL_QNAME, - }; - } - - public SequenceType getResultType(SequenceType[] suppliedArgumentTypes) { - return SequenceType.NODE_SEQUENCE; + + private static final BrailleCssParser parser = BrailleCssParser.getInstance(); + + public ParseStylesheetDefinition() { + super(ParseStylesheet.class); } - public ExtensionFunctionCall makeCallExpression() { - return new ExtensionFunctionCall() { - public Sequence call(XPathContext context, Sequence[] arguments) throws XPathException { - if (arguments.length == 0) - return EmptySequence.getInstance(); - Item arg = arguments[0].head(); - if (arg == null) - return EmptySequence.getInstance(); - boolean deep = arguments.length > 1 - ? ((BooleanValue)arguments[1]).getBooleanValue() - : false; - Context styleCtxt = Context.ELEMENT; { - if (arguments.length > 2) { - Item i = arguments[2].head(); - if (i != null) { - QName qn = ((QNameValue)i).toJaxpQName(); - if (qn.equals(PAGE)) - styleCtxt = Context.PAGE; - else if (qn.equals(VOLUME)) - styleCtxt = Context.VOLUME; - else if (qn.equals(HYPHENATION_RESOURCE)) - styleCtxt = Context.HYPHENATION_RESOURCE; - else if (qn.equals(VENDOR_RULE)) - styleCtxt = Context.VENDOR_RULE; - else - throw new RuntimeException(); }}} - List result = new ArrayList<>(); - try { - Style style = Style.of(new InlineStyle(arg.getStringValue(), styleCtxt)); - BrailleCssSerializer.toXml( - style, - new SaxonOutputValue( - item -> { - if (item instanceof XdmNode) - result.add(((XdmNode)item).getUnderlyingNode()); - else - throw new RuntimeException(); // should not happen - }, - context.getConfiguration() - ).asXMLStreamWriter(), - deep); - } catch (Exception e) { - logger.error("Error happened while parsing " + arg, e); + public static class ParseStylesheet { + + public static Optional - -

-

+ +

+

@@ -71,9 +71,9 @@ } - -

-

+ +

+

diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_extract.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_extract.xprocspec index 1723fcd950..1161b426ac 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_extract.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_extract.xprocspec @@ -68,46 +68,46 @@ - @page page-1 { - @top-left { - content: "foo" - } -} -@page page-2 { - @top-left { - content: "foo" - } -} -@page page-2:left { - @top-left { - content: "foo" - } - @top-right { - content: "bar" - } -} -#a { - page: page-1 + #a { + page: page-2; } #b { display: block; - page: page-2 + page: page-1; } #c { - display: block; border-bottom-align: center; border-bottom-pattern: ⠒; border-bottom-style: none; - border-bottom-width: 1 + border-bottom-width: 1; + display: block; } #d, #e { - display: block + display: block; } #f { - display: block + display: block; } #f::before { - content: "foo" + content: 'foo'; +} +@page page-1 { + @top-left { + content: 'foo'; + } +} +@page page-1:left { + @top-left { + content: 'foo'; + } + @top-right { + content: 'bar'; + } +} +@page page-2 { + @top-left { + content: 'foo'; + } } diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_bom.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_bom.xprocspec index ff469e22a1..5541d9d163 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_bom.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_bom.xprocspec @@ -34,7 +34,7 @@ -

+

diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_content.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_content.xprocspec index 619a38b1ae..17b580ed91 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_content.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_content.xprocspec @@ -38,8 +38,8 @@ - - + +
foobarfoobar
@@ -73,7 +73,7 @@ - + @@ -112,7 +112,7 @@ - + foo @@ -146,7 +146,7 @@ -
+
@@ -183,7 +183,7 @@ - foo + foo diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_context.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_context.xprocspec index 1c3c323131..d4cd5ef849 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_context.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_context.xprocspec @@ -70,7 +70,7 @@ - foobar + foobar diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_counter-style.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_counter-style.xprocspec index 1795394a12..410d5a82a9 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_counter-style.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_counter-style.xprocspec @@ -38,7 +38,7 @@ + equals="'@counter-style decimal { symbols: ''0'' ''1'' ''2'' ''3'' ''4'' ''5'' ''6'' ''7'' ''8'' ''9''; system: numeric } @counter-style lower-roman { additive-symbols: 1000 m, 900 cm, 500 d, 400 cd, 100 c, 90 xc, 50 l, 40 xl, 10 x, 9 ix, 5 v, 4 iv, 1 i; range: 1 3999; system: additive }'"/> diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_encoding.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_encoding.xprocspec index bec93b4845..28932bb00e 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_encoding.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_encoding.xprocspec @@ -32,7 +32,7 @@ -

+

@@ -70,7 +70,7 @@ -

+

@@ -107,7 +107,7 @@ -

+

@@ -145,7 +145,7 @@ -

+

@@ -183,7 +183,7 @@ -

+

diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_extension.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_extension.xprocspec index 6331d01ddb..008177f411 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_extension.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_extension.xprocspec @@ -14,13 +14,15 @@

+

@@ -32,8 +34,9 @@ -

-

+

+

+

@@ -49,8 +52,8 @@ @@ -68,7 +71,7 @@

-

+

@@ -84,15 +87,13 @@

-

@@ -105,8 +106,7 @@

-

-

+

@@ -122,8 +122,8 @@ @@ -141,7 +141,7 @@

-

+

diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_hyphenation-resource.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_hyphenation-resource.xprocspec index 2c58451024..6293b34827 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_hyphenation-resource.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_hyphenation-resource.xprocspec @@ -27,7 +27,7 @@ + '") }')"/> diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_import.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_import.xprocspec index 62eeef18d5..644f76f4c8 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_import.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_import.xprocspec @@ -32,8 +32,8 @@ -

-

+

+

@@ -67,7 +67,7 @@

-

+

@@ -99,7 +99,7 @@

-

+

diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_inherit.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_inherit.xprocspec index 6b15d6929a..ecd9c50d5a 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_inherit.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_inherit.xprocspec @@ -27,9 +27,9 @@ - + foo -

bar

+

bar

diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_invalid.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_invalid.xprocspec index be2493c9fa..3285fa33ad 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_invalid.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_invalid.xprocspec @@ -64,7 +64,7 @@ -

+

@@ -98,7 +98,7 @@ - + diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_media.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_media.xprocspec index 0dae1398a2..ee4f81d233 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_media.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_media.xprocspec @@ -53,7 +53,7 @@ -

+

foobar

@@ -71,7 +71,7 @@

- foobar + foobar

@@ -116,7 +116,7 @@ -

+

foobar

@@ -159,7 +159,7 @@ -

+

foobar

@@ -202,7 +202,7 @@ -

+

foobar

@@ -247,7 +247,7 @@ -

+

foobar

@@ -287,7 +287,7 @@ - + foobar diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_namespace.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_namespace.xprocspec index c91521ab97..a8b966a1c5 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_namespace.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_namespace.xprocspec @@ -42,11 +42,11 @@ -

-

-

-

-

+

+

+

+

+

@@ -92,11 +92,11 @@ -

-

-

-

-

+

+

+

+

+

@@ -144,10 +144,10 @@ -

-

-

-

+

+

+

+

@@ -179,7 +179,7 @@ -

+

diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_page.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_page.xprocspec index eea3ab08c1..dc561ceb72 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_page.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_page.xprocspec @@ -48,7 +48,7 @@ - +

foobar

@@ -119,7 +119,7 @@ - +

foobar

@@ -155,7 +155,7 @@ - + @@ -185,7 +185,7 @@ - + @@ -217,7 +217,7 @@ - + @@ -247,7 +247,7 @@ - + @@ -275,7 +275,7 @@ + equals="'@page:left { margin-top: 2 }'"/> @@ -300,7 +300,30 @@ + equals="'@page { @footnotes { border-top-align: center; border-top-pattern: ⠒; border-top-style: none; border-top-width: 1 } }'"/> + + + + + + + + + + + + + + + + + + diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_properties.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_properties.xprocspec index 7a6fde350e..430278d80b 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_properties.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_properties.xprocspec @@ -32,8 +32,8 @@ - foo -

bar

+ foo +

bar

@@ -75,12 +75,12 @@ -

-

-

-

+

+

+

+

-

+

@@ -112,7 +112,7 @@ -

foo

+

foo

@@ -144,8 +144,8 @@ - - 1 + + 1 @@ -181,9 +181,9 @@ - -

-

+ +

+

diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_resolver.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_resolver.xprocspec index 059b414a95..197bf28fe7 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_resolver.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_resolver.xprocspec @@ -26,8 +26,8 @@ -

-

+

+

@@ -57,7 +57,7 @@

-

+

diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_sass.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_sass.xprocspec index b2a0c2e691..987143595e 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_sass.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_sass.xprocspec @@ -29,7 +29,7 @@

-

+

@@ -60,7 +60,7 @@ -

+

@@ -95,7 +95,7 @@

-

+

@@ -126,8 +126,8 @@ -

-

+

+

@@ -159,7 +159,7 @@

-

+

@@ -191,7 +191,7 @@

-

+

@@ -223,7 +223,7 @@

-

+

@@ -255,7 +255,7 @@

-

+

@@ -287,7 +287,7 @@

-

+

@@ -320,7 +320,7 @@

-

+

@@ -357,7 +357,7 @@

-

+

@@ -395,7 +395,7 @@ -

+

@@ -442,7 +442,7 @@ -
+
@@ -486,7 +486,7 @@ -
+
@@ -531,7 +531,7 @@ -
+
diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_selectors.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_selectors.xprocspec index 4e546aa3ed..0aeef05392 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_selectors.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_selectors.xprocspec @@ -30,9 +30,9 @@ - - foo -

foo

+ + foo +

foo

@@ -64,7 +64,7 @@ -

foo

+

foo

@@ -101,7 +101,7 @@

foo

-

foo

+

foo

@@ -140,7 +140,7 @@
-

foo

+

foo

foo

@@ -179,7 +179,7 @@ foo -

foo

+

foo

foo

@@ -222,12 +222,12 @@ - -

-

-

-

-

+ +

+

+

+

+

foo

@@ -264,10 +264,10 @@ - -

+ +

-

+

@@ -303,8 +303,8 @@ -

-

+

+

@@ -352,16 +352,16 @@

    -
  • +
  • -
  • +
    1. -
    2. +
  • -
  • +
- +
@@ -397,7 +397,7 @@ -

foobar

+

foobar

@@ -414,19 +414,16 @@ @@ -443,7 +440,7 @@ -
+
@@ -474,7 +471,7 @@ - + @@ -518,7 +515,7 @@ -
+
@@ -560,9 +557,9 @@

-

foo

+

foo

-

foo

+

foo

diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_specificity.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_specificity.xprocspec index 035ee9f09b..e1fbc2baa5 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_specificity.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_specificity.xprocspec @@ -48,7 +48,7 @@

-

+

diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_text-transform.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_text-transform.xprocspec index 660b09b65c..ebcca6a110 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_text-transform.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_text-transform.xprocspec @@ -35,7 +35,7 @@ + equals="'@text-transform foo { system: -foo-transform; x: 1; y: 2; z: ''\27 3\27 '' }'"/> @@ -43,7 +43,7 @@

- foo bar + foo bar

diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_user-stylesheet.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_user-stylesheet.xprocspec index 99955fa1fe..ca5c712eae 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_user-stylesheet.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_user-stylesheet.xprocspec @@ -24,8 +24,8 @@ -

-

+

+

@@ -51,8 +51,8 @@ -

-

+

+

@@ -83,8 +83,8 @@ -

-

+

+

diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_volume.xprocspec b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_volume.xprocspec index 447807c921..c0662115cb 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_volume.xprocspec +++ b/modules/braille/braille-css-utils/src/test/xprocspec/test_inline/test_volume.xprocspec @@ -34,7 +34,7 @@ + equals="'@volume { max-length: 100; min-length: 80 } @volume:last { max-length: 100; min-length: 0 } @volume:only { max-length: 120; min-length: 0 } @volume:nth(2) { max-length: auto; min-length: 80 } @volume:first { max-length: 120; min-length: 80 }'"/> @@ -65,7 +65,7 @@ + equals='"@volume { max-length: 100; @begin { content: flow(toc) } } @volume:first { max-length: 100; @begin { content: flow(cover) flow(toc) } }"'/> @@ -93,7 +93,7 @@ + equals="'@volume { max-length: 75; @begin { content: flow(volume-toc) } }'"/> @@ -130,7 +130,7 @@ + equals="'@page { size: 15 5 } @volume { max-length: 75; @begin { content: flow(frontmatter); @page { size: 15 5; @top-right { content: counter(page) } } } }'"/> @@ -156,58 +156,7 @@ - - - - - - - - - - - - - -

-
-
-
- - - - - - - - - - - - - - - -
-
-
-
- - - - - + equals="'@volume:first { max-length: 75 }'"/> @@ -217,7 +166,7 @@ + + + + + + + + + + + + + + application/x-pef+xml + + + + +
+ + ⠋⠀⠕⠀⠕⠀⠃⠀⠁⠀⠗ + +
+
+ +
+
+
+
+ diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_label-targets.xprocspec b/modules/braille/dotify-utils/src/test/xprocspec/test_label-targets.xprocspec similarity index 96% rename from modules/braille/braille-css-utils/src/test/xprocspec/test_label-targets.xprocspec rename to modules/braille/dotify-utils/src/test/xprocspec/test_label-targets.xprocspec index a8e288d15d..813e52b382 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_label-targets.xprocspec +++ b/modules/braille/dotify-utils/src/test/xprocspec/test_label-targets.xprocspec @@ -1,11 +1,12 @@ - + @@ -75,7 +76,7 @@ - + @@ -111,7 +112,7 @@ - + diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_make-anonymous-block-boxes.xprocspec b/modules/braille/dotify-utils/src/test/xprocspec/test_make-anonymous-block-boxes.xprocspec similarity index 93% rename from modules/braille/braille-css-utils/src/test/xprocspec/test_make-anonymous-block-boxes.xprocspec rename to modules/braille/dotify-utils/src/test/xprocspec/test_make-anonymous-block-boxes.xprocspec index 72052cb778..b293092c96 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_make-anonymous-block-boxes.xprocspec +++ b/modules/braille/dotify-utils/src/test/xprocspec/test_make-anonymous-block-boxes.xprocspec @@ -1,10 +1,11 @@ - + @@ -44,7 +45,7 @@ - + diff --git a/modules/braille/dotify-utils/src/test/xprocspec/test_new-definition.xprocspec b/modules/braille/dotify-utils/src/test/xprocspec/test_new-definition.xprocspec new file mode 100644 index 0000000000..995e6e4f4c --- /dev/null +++ b/modules/braille/dotify-utils/src/test/xprocspec/test_new-definition.xprocspec @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_shift-id.xprocspec b/modules/braille/dotify-utils/src/test/xprocspec/test_shift-id.xprocspec similarity index 95% rename from modules/braille/braille-css-utils/src/test/xprocspec/test_shift-id.xprocspec rename to modules/braille/dotify-utils/src/test/xprocspec/test_shift-id.xprocspec index aa34126391..cfa913e32f 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_shift-id.xprocspec +++ b/modules/braille/dotify-utils/src/test/xprocspec/test_shift-id.xprocspec @@ -1,11 +1,12 @@ - + <_> @@ -59,7 +60,7 @@ - + diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_shift-string-set.xprocspec b/modules/braille/dotify-utils/src/test/xprocspec/test_shift-string-set.xprocspec similarity index 94% rename from modules/braille/braille-css-utils/src/test/xprocspec/test_shift-string-set.xprocspec rename to modules/braille/dotify-utils/src/test/xprocspec/test_shift-string-set.xprocspec index 895c034dde..5f9b6c5891 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_shift-string-set.xprocspec +++ b/modules/braille/dotify-utils/src/test/xprocspec/test_shift-string-set.xprocspec @@ -1,11 +1,12 @@ - + @@ -47,7 +48,7 @@ - + diff --git a/modules/braille/braille-css-utils/src/test/xprocspec/test_split.xprocspec b/modules/braille/dotify-utils/src/test/xprocspec/test_split.xprocspec similarity index 97% rename from modules/braille/braille-css-utils/src/test/xprocspec/test_split.xprocspec rename to modules/braille/dotify-utils/src/test/xprocspec/test_split.xprocspec index 96ebc2ba81..78d82beae5 100644 --- a/modules/braille/braille-css-utils/src/test/xprocspec/test_split.xprocspec +++ b/modules/braille/dotify-utils/src/test/xprocspec/test_split.xprocspec @@ -2,11 +2,12 @@ - + @@ -59,7 +60,7 @@ - + @@ -90,7 +91,7 @@ - + @@ -152,7 +153,7 @@ - + @@ -237,7 +238,7 @@ - + diff --git a/modules/braille/braille-css-utils/src/test/xspec/test_adjust-boxes.xspec b/modules/braille/dotify-utils/src/test/xspec/test_adjust-boxes.xspec similarity index 100% rename from modules/braille/braille-css-utils/src/test/xspec/test_adjust-boxes.xspec rename to modules/braille/dotify-utils/src/test/xspec/test_adjust-boxes.xspec diff --git a/modules/braille/dotify-utils/src/test/xspec/test_generate-obfl-layout-master.xspec b/modules/braille/dotify-utils/src/test/xspec/test_generate-obfl-layout-master.xspec index 58f2fd2829..7cf549066e 100644 --- a/modules/braille/dotify-utils/src/test/xspec/test_generate-obfl-layout-master.xspec +++ b/modules/braille/dotify-utils/src/test/xspec/test_generate-obfl-layout-master.xspec @@ -1,6 +1,7 @@ @@ -11,9 +12,7 @@ - - - + @@ -29,23 +28,18 @@ - - - - - - - - - - - - - - - - - + @@ -78,33 +72,24 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -150,14 +135,13 @@ - - - - - - - - + @@ -180,14 +164,13 @@ - - - - - - - - + @@ -218,19 +201,16 @@ - - - - - - - - - - - - - + @@ -253,19 +233,16 @@ - - - - - - - - - - - - - + @@ -296,20 +273,17 @@ - - - - - - - - - - - - - - + @@ -340,20 +314,17 @@ - - - - - - - - - - - - - - + @@ -384,35 +355,26 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -458,20 +420,17 @@ - - - - - - - - - - - - - - + @@ -490,32 +449,25 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/modules/braille/braille-css-utils/src/test/xspec/test_make-anonymous-inline-boxes.xspec b/modules/braille/dotify-utils/src/test/xspec/test_make-anonymous-inline-boxes.xspec similarity index 100% rename from modules/braille/braille-css-utils/src/test/xspec/test_make-anonymous-inline-boxes.xspec rename to modules/braille/dotify-utils/src/test/xspec/test_make-anonymous-inline-boxes.xspec diff --git a/modules/braille/braille-css-utils/src/test/xspec/test_make-boxes.xspec b/modules/braille/dotify-utils/src/test/xspec/test_make-boxes.xspec similarity index 98% rename from modules/braille/braille-css-utils/src/test/xspec/test_make-boxes.xspec rename to modules/braille/dotify-utils/src/test/xspec/test_make-boxes.xspec index 1db040d20d..d28ad16764 100644 --- a/modules/braille/braille-css-utils/src/test/xspec/test_make-boxes.xspec +++ b/modules/braille/dotify-utils/src/test/xspec/test_make-boxes.xspec @@ -38,7 +38,7 @@ - + diff --git a/modules/braille/braille-css-utils/src/test/xspec/test_make-pseudo-elements.xspec b/modules/braille/dotify-utils/src/test/xspec/test_make-pseudo-elements.xspec similarity index 100% rename from modules/braille/braille-css-utils/src/test/xspec/test_make-pseudo-elements.xspec rename to modules/braille/dotify-utils/src/test/xspec/test_make-pseudo-elements.xspec diff --git a/modules/braille/braille-css-utils/src/test/xspec/test_make-table-grid.xspec b/modules/braille/dotify-utils/src/test/xspec/test_make-table-grid.xspec similarity index 100% rename from modules/braille/braille-css-utils/src/test/xspec/test_make-table-grid.xspec rename to modules/braille/dotify-utils/src/test/xspec/test_make-table-grid.xspec diff --git a/modules/braille/braille-css-utils/src/test/xspec/test_padding-to-margin.xspec b/modules/braille/dotify-utils/src/test/xspec/test_padding-to-margin.xspec similarity index 100% rename from modules/braille/braille-css-utils/src/test/xspec/test_padding-to-margin.xspec rename to modules/braille/dotify-utils/src/test/xspec/test_padding-to-margin.xspec diff --git a/modules/braille/braille-css-utils/src/test/xspec/test_preserve-white-space.xspec b/modules/braille/dotify-utils/src/test/xspec/test_preserve-white-space.xspec similarity index 100% rename from modules/braille/braille-css-utils/src/test/xspec/test_preserve-white-space.xspec rename to modules/braille/dotify-utils/src/test/xspec/test_preserve-white-space.xspec diff --git a/modules/braille/dotify-utils/src/test/xspec/test_resolve-css-string.xspec b/modules/braille/dotify-utils/src/test/xspec/test_resolve-css-string.xspec new file mode 100644 index 0000000000..ec0bf84eff --- /dev/null +++ b/modules/braille/dotify-utils/src/test/xspec/test_resolve-css-string.xspec @@ -0,0 +1,31 @@ + + + + + page + + + + + + + + + +

+ +

+ +

+ + + + + + + + + diff --git a/modules/braille/dotify-utils/src/test/xspec/test_round-line-height.xspec b/modules/braille/dotify-utils/src/test/xspec/test_round-line-height.xspec new file mode 100644 index 0000000000..04e9a32af2 --- /dev/null +++ b/modules/braille/dotify-utils/src/test/xspec/test_round-line-height.xspec @@ -0,0 +1,32 @@ + + + + + page + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/braille/dotify-utils/src/test/xspec/test_volume-stylesheets-use-when.xspec b/modules/braille/dotify-utils/src/test/xspec/test_volume-stylesheets-use-when.xspec index 126f6095ad..1ddbdff0c7 100644 --- a/modules/braille/dotify-utils/src/test/xspec/test_volume-stylesheets-use-when.xspec +++ b/modules/braille/dotify-utils/src/test/xspec/test_volume-stylesheets-use-when.xspec @@ -1,5 +1,6 @@ @@ -10,17 +11,12 @@ - - - - - - - - - - - + + styledText(String... textAndStyle) { List styledText = new ArrayList(); @@ -105,7 +108,7 @@ private Iterable styledText(String... textAndStyle) { boolean textSet = false; for (String s : textAndStyle) { if (textSet) - styledText.add(new CSSStyledText(text, s)); + styledText.add(new CSSStyledText(text, cssParser.parse(s))); else text = s; textSet = !textSet; } @@ -117,7 +120,7 @@ private Iterable styledText(String... textAndStyle) { private Iterable text(String... text) { List styledText = new ArrayList(); for (String t : text) - styledText.add(new CSSStyledText(t, "")); + styledText.add(new CSSStyledText(t, cssParser.parse(""))); return styledText; } diff --git a/modules/braille/liblouis-utils/pom.xml b/modules/braille/liblouis-utils/pom.xml index 858b04ba17..eb744a552f 100644 --- a/modules/braille/liblouis-utils/pom.xml +++ b/modules/braille/liblouis-utils/pom.xml @@ -85,23 +85,17 @@ org.slf4j slf4j-api - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime - - - -

- foobar -

-

- foobar foobar -

- - - - -

⠋⠕⠕­⠃⠜

-

⠋⠕⠕⠃⠜ ⠋⠕⠕­⠃⠜

-
-
-
- -
diff --git a/modules/braille/pef-utils/pom.xml b/modules/braille/pef-utils/pom.xml index c786db28e9..7c0ea5a2b0 100644 --- a/modules/braille/pef-utils/pom.xml +++ b/modules/braille/pef-utils/pom.xml @@ -62,12 +62,17 @@ com.openhtmltopdf openhtmltopdf-slf4j + + org.daisy.pipeline.modules + file-utils + org.liblouis liblouis-java + runtime org.daisy.pipeline.modules common-utils - runtime diff --git a/modules/common/file-utils/src/main/resources/META-INF/catalog.xml b/modules/common/file-utils/src/main/resources/META-INF/catalog.xml index f63d56f941..39d846c776 100644 --- a/modules/common/file-utils/src/main/resources/META-INF/catalog.xml +++ b/modules/common/file-utils/src/main/resources/META-INF/catalog.xml @@ -7,6 +7,4 @@ - - diff --git a/modules/common/fileset-utils/pom.xml b/modules/common/fileset-utils/pom.xml index 980faa62ba..c9f770f8f3 100644 --- a/modules/common/fileset-utils/pom.xml +++ b/modules/common/fileset-utils/pom.xml @@ -37,18 +37,13 @@ org.daisy.pipeline calabash-adapter - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules zip-utils - runtime diff --git a/modules/common/fileset-utils/src/main/resources/META-INF/catalog.xml b/modules/common/fileset-utils/src/main/resources/META-INF/catalog.xml index c6333c0697..dfdf9885b3 100755 --- a/modules/common/fileset-utils/src/main/resources/META-INF/catalog.xml +++ b/modules/common/fileset-utils/src/main/resources/META-INF/catalog.xml @@ -5,8 +5,4 @@ - - - - diff --git a/modules/common/mediatype-utils/pom.xml b/modules/common/mediatype-utils/pom.xml index 74471f2867..da2c0850e4 100644 --- a/modules/common/mediatype-utils/pom.xml +++ b/modules/common/mediatype-utils/pom.xml @@ -17,13 +17,9 @@ DAISY Pipeline 2 module :: mediatype-utils - org.daisy.pipeline.modules fileset-utils - runtime diff --git a/modules/common/mediatype-utils/src/main/resources/META-INF/catalog.xml b/modules/common/mediatype-utils/src/main/resources/META-INF/catalog.xml index 1ea7cf6c26..ab486eb81f 100755 --- a/modules/common/mediatype-utils/src/main/resources/META-INF/catalog.xml +++ b/modules/common/mediatype-utils/src/main/resources/META-INF/catalog.xml @@ -2,5 +2,4 @@ - diff --git a/modules/common/validation-utils/pom.xml b/modules/common/validation-utils/pom.xml index 065ffd13a1..5670df6eb4 100644 --- a/modules/common/validation-utils/pom.xml +++ b/modules/common/validation-utils/pom.xml @@ -17,26 +17,16 @@ DAISY Pipeline 2 module :: validation-utils - - - org.daisy.pipeline.modules - file-utils - runtime - org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules common-utils - runtime org.daisy.libs diff --git a/modules/common/validation-utils/src/main/resources/META-INF/catalog.xml b/modules/common/validation-utils/src/main/resources/META-INF/catalog.xml index 75f066d604..e06b66d437 100755 --- a/modules/common/validation-utils/src/main/resources/META-INF/catalog.xml +++ b/modules/common/validation-utils/src/main/resources/META-INF/catalog.xml @@ -2,7 +2,4 @@ - - - diff --git a/modules/nlp/dtbook-break-detection/pom.xml b/modules/nlp/dtbook-break-detection/pom.xml index bdc8da06bf..6bd478a628 100644 --- a/modules/nlp/dtbook-break-detection/pom.xml +++ b/modules/nlp/dtbook-break-detection/pom.xml @@ -19,13 +19,9 @@ Performs sentence and word detection on a DTBOOK document - org.daisy.pipeline.modules nlp-common - runtime diff --git a/modules/nlp/dtbook-break-detection/src/main/resources/META-INF/catalog.xml b/modules/nlp/dtbook-break-detection/src/main/resources/META-INF/catalog.xml index 98c0bdc4f0..71bbcbe1c4 100755 --- a/modules/nlp/dtbook-break-detection/src/main/resources/META-INF/catalog.xml +++ b/modules/nlp/dtbook-break-detection/src/main/resources/META-INF/catalog.xml @@ -2,7 +2,4 @@ - - - diff --git a/modules/nlp/html-break-detection/pom.xml b/modules/nlp/html-break-detection/pom.xml index 5cb9915d7c..1a62feecce 100644 --- a/modules/nlp/html-break-detection/pom.xml +++ b/modules/nlp/html-break-detection/pom.xml @@ -19,13 +19,9 @@ Performs sentence and word detection on a HTML document - org.daisy.pipeline.modules nlp-common - runtime org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules nlp-common - runtime diff --git a/modules/nlp/zedai-break-detection/src/main/resources/META-INF/catalog.xml b/modules/nlp/zedai-break-detection/src/main/resources/META-INF/catalog.xml index 61c460b419..718fe76278 100755 --- a/modules/nlp/zedai-break-detection/src/main/resources/META-INF/catalog.xml +++ b/modules/nlp/zedai-break-detection/src/main/resources/META-INF/catalog.xml @@ -2,7 +2,4 @@ - - - diff --git a/modules/parent/pom.xml b/modules/parent/pom.xml index c74c9de020..37d255011f 100644 --- a/modules/parent/pom.xml +++ b/modules/parent/pom.xml @@ -149,7 +149,7 @@ org.daisy.pipeline.build modules-build-helper - 2.8.0 + 3.0.0-SNAPSHOT org.apache.maven.plugins @@ -159,7 +159,7 @@ org.daisy.pipeline.build ds-to-spi-maven-plugin - 1.1.4 + 1.1.5-SNAPSHOT org.apache.felix @@ -203,6 +203,11 @@ xproc-maven-plugin 1.0.3 + + org.daisy.maven + xprocspec-runner + 1.2.8-SNAPSHOT + org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules diff --git a/modules/scripts-utils/ace-adapter/src/main/resources/META-INF/catalog.xml b/modules/scripts-utils/ace-adapter/src/main/resources/META-INF/catalog.xml index 410ad861b7..093c6291d3 100644 --- a/modules/scripts-utils/ace-adapter/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts-utils/ace-adapter/src/main/resources/META-INF/catalog.xml @@ -1,6 +1,4 @@ - - diff --git a/modules/scripts-utils/css-utils/pom.xml b/modules/scripts-utils/css-utils/pom.xml index c226a76c7d..8320175c8e 100644 --- a/modules/scripts-utils/css-utils/pom.xml +++ b/modules/scripts-utils/css-utils/pom.xml @@ -11,7 +11,7 @@ css-utils - 5.4.1-SNAPSHOT + 6.0.0-SNAPSHOT bundle DAISY Pipeline 2 module :: CSS Utils @@ -38,6 +38,18 @@ org.daisy.pipeline common-utils + + org.daisy.pipeline.modules + file-utils + + + org.daisy.pipeline.modules + fileset-utils + + + org.daisy.pipeline.modules + mediatype-utils + org.daisy.libs io.bit3.jsass @@ -106,24 +118,6 @@ org.slf4j slf4j-api - - - org.daisy.pipeline.modules - file-utils - runtime - - - org.daisy.pipeline.modules - fileset-utils - runtime - - - org.daisy.pipeline.modules - mediatype-utils - runtime - diff --git a/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/CounterEvaluator.java b/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/CounterEvaluator.java new file mode 100644 index 0000000000..6de7ea30b5 --- /dev/null +++ b/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/CounterEvaluator.java @@ -0,0 +1,362 @@ +package org.daisy.pipeline.css; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; + +import cz.vutbr.web.css.Term; +import cz.vutbr.web.css.TermFunction; +import cz.vutbr.web.css.TermIdent; +import cz.vutbr.web.css.TermPair; +import cz.vutbr.web.css.TermString; + +import org.daisy.braille.css.BrailleCSSProperty.Display; + +/** + * Traverses a document while keeping track of counter values and providing the ability to + * generate counter representations. + */ +public abstract class CounterEvaluator { + + /** + * Get {@code counter-reset} property. + */ + protected abstract Collection> getCounterReset(E elementStyle); + + /** + * Get {@code counter-set} property. + */ + protected abstract Collection> getCounterSet(E elementStyle); + + /** + * Get {@code counter-increment} property. + */ + protected abstract Collection> getCounterIncrement(E elementStyle); + + /** + * Get {@code display} property. + */ + protected abstract Display getDisplay(E elementStyle); + + /** + * Get {@code content} property of {@code ::marker} pseudo-element. + * + * {@code null} means there is no {@code ::marker} pseudo-element. An empty list means the + * pseudo-element has no content. + */ + protected abstract Collection> getMarkerContent(E elementStyle); + + /** + * Get list counter style. + */ + protected abstract CounterStyle getListStyleType(E elementStyle, CounterStyle parentListStyleType); + + /** + * Get counter style for given name + */ + protected abstract CounterStyle getNamedCounterStyle(String name); + + private final LinkedList listCounterStyle = new LinkedList<>(); + protected final LinkedList> counterValues = new LinkedList<>(); + private boolean previousEventWasStartElement = true; // false = endElement + + /** + * This method must be called at the start of every element while traversing the document tree + * in depth-first order. + * + * New counter values are determined based on the given style. + * + * @param elementStyle The style of the current element, possibly containing {@code + * counter-reset}, {@code counter-set} and {@code counter-increment} + * properties. + */ + public void startElement(E elementStyle) { + if (listCounterStyle.isEmpty()) + listCounterStyle.push(CounterStyle.DISC); + if (previousEventWasStartElement) + counterValues.push(null); + previousEventWasStartElement = true; + updateCounterValues(elementStyle); + updateCurrentListCounterStyle(elementStyle); + } + + /** + * This method must be called at the end of every element while traversing the document tree in + * depth-first order. + */ + public void endElement() { + listCounterStyle.pop(); + if (!previousEventWasStartElement) + counterValues.pop(); + previousEventWasStartElement = false; + } + + /** + * Determine current counter values (see https://www.w3.org/TR/css-lists-3/#creating-counters) + */ + private void updateCounterValues(E elementStyle) { + // counter-reset + { + Collection> list = getCounterReset(elementStyle); + if (list != null) { + for (TermPair p : list) { + String name = p.getKey(); + Map createdBySiblingOrSelf = counterValues.peek(); + if (createdBySiblingOrSelf == null) { + createdBySiblingOrSelf = new HashMap<>(); + counterValues.set(0, createdBySiblingOrSelf); + } + createdBySiblingOrSelf.put(name, p.getValue()); + } + } + } + // counter-set or counter-increment work in addition to counter-reset + Set done = null; + { + Collection> list = getCounterSet(elementStyle); + if (list != null) { + for (TermPair p : list) { + String name = p.getKey(); + boolean exists = false; + for (Map c : counterValues) { + if (c != null) { + c.put(name, p.getValue()); + exists = true; + break; + } + } + if (!exists) { + Map createdBySiblingOrSelf = counterValues.peek(); + if (createdBySiblingOrSelf == null) { + createdBySiblingOrSelf = new HashMap<>(); + counterValues.set(0, createdBySiblingOrSelf); + } + createdBySiblingOrSelf.put(name, p.getValue()); + } + if (done == null) + done = new HashSet<>(); + done.add(name); + } + } + } + { + Collection> list = getCounterIncrement(elementStyle); + if (list != null) { + for (TermPair p : list) { + String name = p.getKey(); + if (done == null || !done.contains(name)) { + boolean exists = false; + for (Map c : counterValues) { + if (c != null) { + c.put(name, c.getOrDefault(name, 0) + p.getValue()); + exists = true; + break; + } + } + if (!exists) { + Map createdBySiblingOrSelf = counterValues.peek(); + if (createdBySiblingOrSelf == null) { + createdBySiblingOrSelf = new HashMap<>(); + counterValues.set(0, createdBySiblingOrSelf); + } + createdBySiblingOrSelf.put(name, p.getValue()); + } + if (done == null) + done = new HashSet<>(); + done.add(name); + } + } + } + } + // by default list items increment the 'list-item' counter + if ((done == null || !done.contains("list-item")) && getDisplay(elementStyle) == Display.LIST_ITEM) { + boolean exists = false; + for (Map c : counterValues) { + if (c != null) { + c.put("list-item", c.getOrDefault("list-item", 0) + 1); + exists = true; + break; + } + } + if (!exists) { + Map createdBySiblingOrSelf = counterValues.peek(); + if (createdBySiblingOrSelf == null) { + createdBySiblingOrSelf = new HashMap<>(); + counterValues.set(0, createdBySiblingOrSelf); + } + createdBySiblingOrSelf.put("list-item", 1); + } + } + } + + /** + * Determine current list counter style + */ + private void updateCurrentListCounterStyle(E elementStyle) { + listCounterStyle.push(getListStyleType(elementStyle, listCounterStyle.peek())); + } + + /** + * Evaluate marker contents (see https://www.w3.org/TR/css-lists-3/#content-property) + */ + public String generateMarkerContents(E elementStyle) throws IllegalArgumentException { + if (getDisplay(elementStyle) == Display.LIST_ITEM) { + Collection> content = getMarkerContent(elementStyle); + if (content != null) { + if (!content.isEmpty()) + return evaluateContent(content); + else + return null; + } + CounterStyle listCounterStyle = this.listCounterStyle.peek(); + if (listCounterStyle != null) + return evaluateCounter("list-item", listCounterStyle, true); + } + return null; + } + + private String evaluateContent(Collection> content) throws IllegalArgumentException { + StringBuilder s = new StringBuilder(); + for (Term t : content) { + if (t instanceof TermString) { + s.append(((TermString)t).getValue()); + } else if (t instanceof TermFunction) { + TermFunction f = (TermFunction)t; + if ("counter".equalsIgnoreCase(f.getFunctionName())) { + String name = null; + CounterStyle style = null; + for (Term arg : f) + if (name == null) + if (arg instanceof TermIdent) + name = ((TermIdent)arg).getValue(); + else + throw new IllegalArgumentException( + "invalid first argument of counter() function: should be the counter name"); + else if (style == null) { + if (arg instanceof TermIdent) + style = getNamedCounterStyle(((TermIdent)arg).getValue()); + else if (arg instanceof TermFunction) + try { + style = CounterStyle.fromSymbolsFunction((TermFunction)arg); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "invalid second argument of counter() function: should be a counter style", e); + } + else + throw new IllegalArgumentException( + "invalid second argument of counter() function: should be a counter style"); } + else + throw new IllegalArgumentException( + "unexpected argument of counter() function: function takes at most two arguments"); + if (name == null) + throw new IllegalArgumentException("counter() function requires at least one argument"); + if (style == null) + style = CounterStyle.DECIMAL; + s.append(evaluateCounter(name, style, false)); + } else if ("counters".equalsIgnoreCase(f.getFunctionName())) { + String name = null; + String separator = null; + CounterStyle style = null; + for (Term arg : f) + if (name == null) + if (arg instanceof TermIdent) + name = ((TermIdent)arg).getValue(); + else + throw new IllegalArgumentException( + "invalid first argument of counters() function: should be the counter name"); + else if (separator == null) { + if (arg instanceof TermString) + separator = ((TermString)arg).getValue(); + else + throw new IllegalArgumentException( + "invalid second argument of counters() function: should be a string"); } + else if (style == null) { + if (arg instanceof TermIdent) + style = getNamedCounterStyle(((TermIdent)arg).getValue()); + else if (arg instanceof TermFunction) + try { + style = CounterStyle.fromSymbolsFunction((TermFunction)arg); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + "invalid third argument of counters() function: should be a counter style", e); + } + else + throw new IllegalArgumentException( + "invalid third argument of counters() function: should be a counter style"); } + else + throw new IllegalArgumentException( + "unexpected argument of counters() function: function takes at most three arguments"); + if (name == null || separator == null) + throw new IllegalArgumentException("counters() function requires at least two arguments"); + if (style == null) + style = CounterStyle.DECIMAL; + s.append(evaluateCounters(name, style, separator)); + } else + throw new RuntimeException(f.getFunctionName() + "() function not supported in content list"); // FIXME + } else + throw new IllegalStateException(); // cannot happen + } + return s.toString(); + } + + /** + * Generate a counter representation of the innermost counter in the counter set with the given + * name (see https://www.w3.org/TR/css-lists-3/#counter-functions) + */ + public String evaluateCounter(String name, CounterStyle style, boolean withPrefixAndSuffix) { + int value = evaluateCounter(name); + return style.format(value, withPrefixAndSuffix); + } + + /** + * Get the value of the innermost counter in the counter set with the given name, or instaniate + * a counter with value {@code 0} (see https://www.w3.org/TR/css-lists-3/#counter-functions) + */ + public int evaluateCounter(String name) { + Integer value = null; + boolean exists = false; + for (Map c : counterValues) + if (c != null && c.containsKey(name)) { + value = c.get(name); + break; + } + if (value == null) { + value = 0; + Map createdBySiblingOrSelf = counterValues.peek(); + if (createdBySiblingOrSelf == null) { + createdBySiblingOrSelf = new HashMap<>(); + counterValues.set(0, createdBySiblingOrSelf); + } + createdBySiblingOrSelf.put(name, value); + } + return value; + } + + /** + * Generate a counter representation of all the counters in the counter set with the given name + * (see https://www.w3.org/TR/css-lists-3/#counter-functions) + */ + public String evaluateCounters(String name, CounterStyle style, String separator) { + StringBuilder s = new StringBuilder(); + for (Map c : counterValues) + if (c != null && c.containsKey(name)) { + if (s.length() > 0) + s.insert(0, separator); + s.insert(0, style.format(c.get(name))); + } + if (s.length() == 0) { + Map createdBySiblingOrSelf = counterValues.peek(); + if (createdBySiblingOrSelf == null) { + createdBySiblingOrSelf = new HashMap<>(); + counterValues.set(0, createdBySiblingOrSelf); + } + createdBySiblingOrSelf.put(name, 0); + s.append(style.format(0)); + } + return s.toString(); + } +} diff --git a/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/CounterStyle.java b/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/CounterStyle.java index ec762acf36..bba97f4864 100644 --- a/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/CounterStyle.java +++ b/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/CounterStyle.java @@ -1,6 +1,7 @@ package org.daisy.pipeline.css; import java.util.ArrayList; +import java.util.Collections; import java.util.function.Supplier; import java.util.HashMap; import java.util.Iterator; @@ -8,6 +9,7 @@ import java.util.Map; import java.util.Optional; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import cz.vutbr.web.css.Declaration; @@ -27,7 +29,66 @@ import org.unbescape.css.CssEscape; -public class CounterStyle { +/** + * See http://www.w3.org/TR/css-counter-styles-3 + */ +public interface CounterStyle { + + public String format(int counterValue); + + public String format(int counterValue, boolean withPrefixAndSuffix); + + @Deprecated + public String getTextTransform(int counterValue); + + // numeric + public final static CounterStyle DECIMAL = CounterStyleImpl.predefinedCounterStyles.get("decimal"); + // alphabetic + public final static CounterStyle LOWER_ALPHA = CounterStyleImpl.predefinedCounterStyles.get("lower-alpha"); + public final static CounterStyle UPPER_ALPHA = CounterStyleImpl.predefinedCounterStyles.get("upper-alpha"); + public final static CounterStyle LOWER_ROMAN = CounterStyleImpl.predefinedCounterStyles.get("lower-roman"); + public final static CounterStyle UPPER_ROMAN = CounterStyleImpl.predefinedCounterStyles.get("upper-roman"); + // cyclic + public final static CounterStyle DISC = CounterStyleImpl.predefinedCounterStyles.get("disc"); + + /** + * Create a CounterStyle from a symbols() function + */ + public static CounterStyle fromSymbolsFunction(Term term) throws IllegalArgumentException { + return CounterStyleImpl.fromSymbolsFunction(term); + } + + /** + * Create a map of named CounterStyle from a set of @counter-style rules. + */ + public static Map parseCounterStyleRules(Iterable style) { + return (Map)(Map)CounterStyleImpl.parseCounterStyleRules(style); + } + + /** + * Return the predefined counter style with the given name, or {@code null} if no counter style + * with that name exists. + */ + public static CounterStyle predefined(String name) { + return CounterStyleImpl.predefinedCounterStyles.get(name); + } + + /** + * Create a constant CounterStyle (cyclic with a single symbol) + */ + public static CounterStyle constant(String symbol) { + return new CounterStyleImpl(symbol); + } + + /** + * Create a cyclic CounterStyle + */ + public static CounterStyle cycle(Iterable symbols) { + return new CounterStyleImpl(ImmutableList.copyOf(symbols)); + } +} + +class CounterStyleImpl implements CounterStyle { private enum System { ALPHABETIC, @@ -38,7 +99,8 @@ private enum System { ADDITIVE }; - private static class AdditiveTuple { + // package private for tests + static class AdditiveTuple { final int weight; final String symbol; AdditiveTuple(int weight, String symbol) { @@ -49,7 +111,7 @@ private static class AdditiveTuple { private final static Logger logger = LoggerFactory.getLogger(CounterStyle.class); - private final static Map predefinedCounterStyles = parseCounterStyleRules( + final static Map predefinedCounterStyles = parseCounterStyleRules( Iterables.filter( new InlineStyle( "@counter-style decimal {" + "\n" + @@ -90,21 +152,12 @@ private static class AdditiveTuple { "}" ), RuleCounterStyle.class)); - // numeric - public final static CounterStyle DECIMAL = predefinedCounterStyles.get("decimal"); - // alphabetic - public final static CounterStyle LOWER_ALPHA = predefinedCounterStyles.get("lower-alpha"); - public final static CounterStyle UPPER_ALPHA = predefinedCounterStyles.get("upper-alpha"); - public final static CounterStyle LOWER_ROMAN = predefinedCounterStyles.get("lower-roman"); - public final static CounterStyle UPPER_ROMAN = predefinedCounterStyles.get("upper-roman"); - // cyclic - public final static CounterStyle DISC = predefinedCounterStyles.get("disc"); - private final System system; private final List symbols; private final List additiveSymbols; private final String negative; - private final Supplier fallback; + private final Supplier fallback; + private final String textTransform; // for braille CSS private final String prefix; private final String suffix; @@ -113,7 +166,7 @@ private static class AdditiveTuple { * * @param term assumed to be a "symbols" function */ - private CounterStyle(TermFunction term) throws IllegalArgumentException { + CounterStyleImpl(TermFunction term) throws IllegalArgumentException { System system; List> symbols; if (term.size() > 0 && term.get(0) instanceof TermIdent) { @@ -129,7 +182,8 @@ private CounterStyle(TermFunction term) throws IllegalArgumentException { this.additiveSymbols = null; this.system = system; this.negative = "-"; - this.fallback = () -> DECIMAL; + this.fallback = () -> (CounterStyleImpl)DECIMAL; + this.textTransform = "auto"; this.prefix = ""; this.suffix = " "; } @@ -138,8 +192,7 @@ private CounterStyle(TermFunction term) throws IllegalArgumentException { * Create a CounterStyle from a @counter-style rule */ // FIXME: the "range" descriptor is not taken into account - // FIXME: the "text-transform" descriptor (from braille CSS) is not taken into account - private CounterStyle(RuleCounterStyle rule, Map fallbacks) throws IllegalArgumentException { + CounterStyleImpl(RuleCounterStyle rule, Map fallbacks) throws IllegalArgumentException { Declaration systemDecl = null; Declaration symbolsDecl = null; Declaration additiveSymbolsDecl = null; @@ -147,6 +200,7 @@ private CounterStyle(RuleCounterStyle rule, Map fallbacks) Declaration fallbackDecl = null; Declaration prefixDecl = null; Declaration suffixDecl = null; + Declaration textTransformDecl = null; for (Declaration d : rule) { String prop = d.getProperty(); if ("system".equals(prop)) { @@ -163,6 +217,8 @@ private CounterStyle(RuleCounterStyle rule, Map fallbacks) if (prefixDecl == null) prefixDecl = d; } else if ("suffix".equals(prop)) { if (suffixDecl == null) suffixDecl = d; + } else if ("text-transform".equals(prop)) { + if (textTransformDecl == null) textTransformDecl = d; } } System system; { @@ -216,6 +272,20 @@ private CounterStyle(RuleCounterStyle rule, Map fallbacks) ident = "decimal"; fallback = ident; } + String textTransform; { + textTransform = null; + if (textTransformDecl != null) { + logger.warn("'text-transform' descriptor in @counter-style rule is deprecated. " + + "Use a 'text-transform' property within the rule containing the 'content' property instead."); + try { + textTransform = readSingleIdent("text-transform", textTransformDecl).getValue(); + } catch (IllegalArgumentException e) { + logger.warn(e.getMessage()); + } + } + if (textTransform == null) + textTransform = "auto"; + } String prefix; { prefix = null; if (prefixDecl != null) @@ -236,7 +306,7 @@ private CounterStyle(RuleCounterStyle rule, Map fallbacks) logger.warn(e.getMessage()); } if (suffix == null) - suffix = ". "; // note that for braille CSS this should be the empty string + suffix = ". "; } this.system = system; this.symbols = symbols; @@ -249,26 +319,45 @@ private CounterStyle(RuleCounterStyle rule, Map fallbacks) else if (predefinedCounterStyles.containsKey(fallback)) return predefinedCounterStyles.get(fallback); else - return DECIMAL; + return (CounterStyleImpl)DECIMAL; } ); + this.textTransform = textTransform; this.prefix = prefix; this.suffix = suffix; } - private static final Map fromSymbolsCache = new HashMap<>(); + /** + * Create a constant CounterStyle (cyclic with a single symbol) + */ + CounterStyleImpl(String symbol) { + this(Collections.singletonList(symbol)); + } /** - * Create a CounterStyle from a symbols() function + * Create a cyclic CounterStyle */ - public static CounterStyle fromSymbolsFunction(Term term) throws IllegalArgumentException { + CounterStyleImpl(List symbols) { + system = System.CYCLIC; + this.symbols = symbols; + textTransform = "none"; + prefix = ""; + suffix = " "; + additiveSymbols = null; + negative = null; + fallback = null; + } + + static final Map fromSymbolsCache = new HashMap<>(); + + static CounterStyle fromSymbolsFunction(Term term) throws IllegalArgumentException { if (term instanceof TermFunction) { TermFunction function = (TermFunction)term; if (function.getFunctionName().equals("symbols")) { String k = function.toString(); CounterStyle s = fromSymbolsCache.get(k); if (s == null) { - s = new CounterStyle(function); + s = new CounterStyleImpl(function); fromSymbolsCache.put(k, s); } return s; @@ -277,21 +366,27 @@ public static CounterStyle fromSymbolsFunction(Term term) throws IllegalArgum throw new IllegalArgumentException("argument must be a symbols() function"); } - /** - * Create a map of named CounterStyle from a set of @counter-style rules. - */ - public static Map parseCounterStyleRules(Iterable style) { - Map namedStyles = new HashMap<>(); + static Map parseCounterStyleRules(Iterable style) { + Map namedStyles = new HashMap<>(); for (RuleCounterStyle rule : style) try { - namedStyles.put(rule.getName(), new CounterStyle(rule, namedStyles)); + namedStyles.put(rule.getName(), new CounterStyleImpl(rule, namedStyles)); } catch (IllegalArgumentException e) { logger.warn(e.getMessage()); } return namedStyles; } + @Override public String format(int counterValue) { + Object o = formatOrFallback(counterValue); + if (o instanceof String) + return (String)o; + CounterStyleImpl fallback = (CounterStyleImpl)o; + return fallback.format(counterValue); + } + + private Object formatOrFallback(int counterValue) { switch (system) { case ALPHABETIC: { Optional formatted = counterRepresentationAlphabetic(counterValue, symbols); @@ -322,9 +417,10 @@ public String format(int counterValue) { } if (fallback.get() == null || fallback.get() == this) throw new IllegalStateException(); // can not happen - return fallback.get().format(counterValue); + return fallback.get(); } + @Override public String format(int counterValue, boolean withPrefixAndSuffix) { // prefix and suffix always come from the specified counter-style, even if the actual // representation is constructed by a fallback style @@ -334,6 +430,15 @@ public String format(int counterValue, boolean withPrefixAndSuffix) { return format(counterValue); } + @Override + public String getTextTransform(int counterValue) { + Object o = formatOrFallback(counterValue); + if (o instanceof String) + return this.textTransform;; + CounterStyleImpl fallback = (CounterStyleImpl)o; + return fallback.textTransform; + } + /* ============ private ========================================================= */ private static System readSystem(List> terms) throws IllegalArgumentException { @@ -351,14 +456,8 @@ private static System readSystem(TermIdent ident) throws IllegalArgumentExceptio private static List readSymbols(List> terms) throws IllegalArgumentException { List symbols = new ArrayList<>(); - for (Term t : terms) { - if (t instanceof TermString) - symbols.add(((TermString)t).getValue()); - else if (t instanceof TermIdent) - symbols.add(CssEscape.unescapeCss(((TermIdent)t).getValue())); - else - throw new IllegalArgumentException("Invalid symbol: " + t); - } + for (Term t : terms) + symbols.add(readSymbol(t)); if (symbols.isEmpty()) throw new IllegalArgumentException("Empty symbols list"); return symbols; @@ -378,11 +477,11 @@ private static List readAdditiveSymbols(List> terms) thro prevWeight = weight; if (tt.hasNext()) { t = tt.next(); - if (t instanceof TermString) { - String symbol = ((TermString)t).getValue(); - symbols.add(new AdditiveTuple(weight, symbol)); - } else - throw new IllegalArgumentException("Invalid additive tuple: expected symbol but got " + t); + try { + symbols.add(new AdditiveTuple(weight, readSymbol(t))); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid additive tuple: expected symbol but got " + t, e); + } } else throw new IllegalArgumentException("Invalid additive tuple: expected symbol"); } else @@ -393,6 +492,17 @@ private static List readAdditiveSymbols(List> terms) thro return symbols; } + private static String readSymbol(Term term) { + if (term instanceof TermString) + return ((TermString)term).getValue(); + else if (term instanceof TermIdent) + return CssEscape.unescapeCss(((TermIdent)term).getValue()); + else if (term instanceof TermInteger) + return "" + ((TermInteger)term).getValue(); + else + throw new IllegalArgumentException("Invalid symbol: " + term); + } + private static TermIdent readSingleIdent(String property, List> terms) throws IllegalArgumentException { if (terms.size() != 1 || !(terms.get(0) instanceof TermIdent)) throw new IllegalArgumentException("Invalid " + property + ": " + CssSerializer.serializeTermList(terms)); diff --git a/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/CssSerializer.java b/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/CssSerializer.java index 670612d613..a36612556e 100644 --- a/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/CssSerializer.java +++ b/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/CssSerializer.java @@ -1,12 +1,14 @@ package org.daisy.pipeline.css; import java.net.URI; +import java.util.Collection; import java.util.function.Function; import java.util.List; import cz.vutbr.web.css.Declaration; import cz.vutbr.web.css.Term; import cz.vutbr.web.css.TermFunction; +import cz.vutbr.web.css.TermIdent; import cz.vutbr.web.css.TermInteger; import cz.vutbr.web.css.TermList; import cz.vutbr.web.css.TermNumber; @@ -53,7 +55,7 @@ else if (term instanceof TermList else if (term instanceof TermPair) { TermPair pair = (TermPair)term; Object val = pair.getValue(); - return "" + pair.getKey() + " " + (val instanceof Term ? toString((Term)val, toStringFunction) : val.toString()); } + return "" + pair.getKey() + " " + (val instanceof Term ? toStringFunction.apply((Term)val) : val.toString()); } else if (term instanceof TermURI) { TermURI termURI = (TermURI)term; URI uri = URLs.asURI(termURI.getValue()); @@ -63,15 +65,18 @@ else if (term instanceof TermURI) { else if (term instanceof TermString) { TermString string = (TermString)term; return "'" + string.getValue().replaceAll("\n", "\\\\A ").replaceAll("'", "\\\\27 ") + "'"; } + else if (term instanceof TermIdent) { + TermIdent ident = (TermIdent)term; + return ident.getValue(); } else return term.toString().replaceAll("^[,/ ]+", ""); } - public static String serializeTermList(List> termList) { + public static String serializeTermList(Collection> termList) { return serializeTermList(termList, t -> toString(t)); } - public static String serializeTermList(List> termList, Function,String> toStringFunction) { + public static String serializeTermList(Collection> termList, Function,String> toStringFunction) { String s = ""; for (Term t : termList) { if (!s.isEmpty()) { diff --git a/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/JStyleParserCssCascader.java b/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/JStyleParserCssCascader.java index a0683c3d28..f91c139d9d 100644 --- a/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/JStyleParserCssCascader.java +++ b/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/JStyleParserCssCascader.java @@ -263,22 +263,27 @@ public URL getURL() { return URLs.asURL(URI.create(info.getBaseURI())); } public int getLineNumber() { - return info.getLineNumber(); + int line = info.getLineNumber(); + if (line > 0) + return line - 1; // line numbers are 1-based in Saxon + else + return -1; } public int getColumnNumber() { - return info.getColumnNumber(); + return info.getColumnNumber(); // -1 } }; } else { + // should not happen return new SourceLocator() { public URL getURL() { return URLs.asURL(URI.create(n.getBaseURI())); } public int getLineNumber() { - return 0; + return -1; } public int getColumnNumber() { - return 0; + return -1; } }; } diff --git a/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/StyleAccessor.java b/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/StyleAccessor.java index 7420b837c0..faf83add82 100644 --- a/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/StyleAccessor.java +++ b/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/StyleAccessor.java @@ -9,7 +9,7 @@ import org.w3c.dom.Element; -public abstract class StyleAccessor { +public interface StyleAccessor { @Component( name = "StyleAccesor", @@ -26,13 +26,13 @@ public Provider() { * href="https://www.w3.org/TR/2013/CR-css-cascade-3-20131003/#specified">specified * value of a CSS property and element. */ - public abstract Optional get(Element element, String property); + public Optional get(Element element, String property); /** * Test whether an element matches a CSS selector. * * @throws IllegalArgumentException if the selector can not be compiled */ - public abstract boolean matches(Element element, String selector); + public boolean matches(Element element, String selector); } diff --git a/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/impl/DefaultCssCascader.java b/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/impl/DefaultCssCascader.java index 91376755fa..4b0cda90d5 100644 --- a/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/impl/DefaultCssCascader.java +++ b/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/impl/DefaultCssCascader.java @@ -1,11 +1,9 @@ package org.daisy.pipeline.css.impl; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Set; import javax.xml.namespace.QName; import javax.xml.stream.XMLStreamException; @@ -19,19 +17,15 @@ import cz.vutbr.web.css.NodeData; import cz.vutbr.web.css.RuleFactory; import cz.vutbr.web.css.Selector.PseudoElement; -import cz.vutbr.web.css.SupportedCSS; import cz.vutbr.web.css.Term; import cz.vutbr.web.css.TermFunction; import cz.vutbr.web.css.TermIdent; import cz.vutbr.web.css.TermList; import cz.vutbr.web.css.TermPair; -import cz.vutbr.web.css.TermString; import cz.vutbr.web.csskit.antlr.CSSParserFactory; import cz.vutbr.web.csskit.RuleFactoryImpl; -import cz.vutbr.web.domassign.DeclarationTransformer; import cz.vutbr.web.domassign.StyleMap; -import org.daisy.braille.css.BrailleCSSDeclarationTransformer; import org.daisy.braille.css.BrailleCSSParserFactory; import org.daisy.braille.css.BrailleCSSProperty.Content; import org.daisy.braille.css.BrailleCSSProperty.Display; @@ -41,6 +35,7 @@ import org.daisy.common.stax.BaseURIAwareXMLStreamWriter; import org.daisy.common.stax.XMLStreamWriterHelper; import org.daisy.common.transform.XMLTransformer; +import org.daisy.pipeline.css.CounterEvaluator; import org.daisy.pipeline.css.CounterStyle; import org.daisy.pipeline.css.CssCascader; import org.daisy.pipeline.css.CssPreProcessor; @@ -100,9 +95,7 @@ public XMLTransformer newInstance(Medium medium, // using braille-css because @counter-style is not supported by jStyleParser private static final CSSParserFactory parserFactory = new BrailleCSSParserFactory(); private static final RuleFactory ruleFactory = RuleFactoryImpl.getInstance(); - private static final SupportedCSS supportedCSS = new SupportedBrailleCSS(false, true); // FIXME: support "list-style" shorthand - private static final DeclarationTransformer declarationTransformer - = new BrailleCSSDeclarationTransformer(supportedCSS); + private static final SupportedBrailleCSS supportedCSS = new SupportedBrailleCSS(false, true); // FIXME: support "list-style" shorthand /* * We can make use of JStyleParserCssCascader for the evaluation of marker contents because to @@ -112,17 +105,14 @@ public XMLTransformer newInstance(Medium medium, private static class Transformer extends JStyleParserCssCascader { private Map namedCounterStyles = null; - private LinkedList listCounterStyle = new LinkedList<>(); - private LinkedList> counterValues = new LinkedList<>(); + private CounterEvaluator evaluator; private final QName markerAttributeName; private final QName markerContentAttributeName; private Transformer(URIResolver resolver, CssPreProcessor preProcessor, XsltProcessor xsltProcessor, String userStyleSheet, Medium medium, QName attributeName, boolean multipleAttrs) { super(resolver, preProcessor, xsltProcessor, userStyleSheet, medium, attributeName, - parserFactory, ruleFactory, supportedCSS, declarationTransformer); - listCounterStyle.push(CounterStyle.DISC); - counterValues.push(null); + parserFactory, ruleFactory, supportedCSS, supportedCSS); if (attributeName == null) { markerAttributeName= null; markerContentAttributeName = null; @@ -139,15 +129,137 @@ private Transformer(URIResolver resolver, CssPreProcessor preProcessor, XsltProc @Override protected void processElement(Element element, StyleMap styleMap, BaseURIAwareXMLStreamWriter writer) throws XMLStreamException, IllegalArgumentException { + if (evaluator == null) + // styleMap is constant so we can define evaluator based on styleMap argument of first call + evaluator = new CounterEvaluator() { + @Override + protected Collection> getCounterReset(Element element) { + NodeData style = styleMap.get(element); + if (style.getProperty("counter-reset", false) == CounterReset.list_values) { + List> list = style.getValue(TermList.class, "counter-reset", false); + if (list != null) { + for (Term t : list) { + if (!(t instanceof TermPair)) + throw new IllegalStateException("coding error"); + TermPair p = (TermPair)t; + if (!(p.getKey() instanceof String)) + throw new IllegalStateException("coding error"); + if (!(p.getValue() instanceof Integer)) + throw new IllegalStateException("coding error"); + } + return (List>)(List)list; + } + } + return null; + } + @Override + protected Collection> getCounterSet(Element element) { + NodeData style = styleMap.get(element); + if (style.getProperty("counter-set", false) == CounterSet.list_values) { + List> list = style.getValue(TermList.class, "counter-set", false); + if (list != null) { + for (Term t : list) { + if (!(t instanceof TermPair)) + throw new IllegalStateException("coding error"); + TermPair p = (TermPair)t; + if (!(p.getKey() instanceof String)) + throw new IllegalStateException("coding error"); + if (!(p.getValue() instanceof Integer)) + throw new IllegalStateException("coding error"); + } + return (List>)(List)list; + } + } + return null; + } + @Override + protected Collection> getCounterIncrement(Element element) { + NodeData style = styleMap.get(element); + if (style.getProperty("counter-increment", false) == CounterIncrement.list_values) { + List> list = style.getValue(TermList.class, "counter-increment", false); + if (list != null) { + for (Term t : list) { + if (!(t instanceof TermPair)) + throw new IllegalStateException("coding error"); + TermPair p = (TermPair)t; + if (!(p.getKey() instanceof String)) + throw new IllegalStateException("coding error"); + if (!(p.getValue() instanceof Integer)) + throw new IllegalStateException("coding error"); + } + return (List>)(List)list; + } + } + return null; + } + @Override + protected Display getDisplay(Element element) { + NodeData style = styleMap.get(element); + return style.getProperty("display", false); + } + @Override + protected Collection> getMarkerContent(Element element) { + NodeData style = styleMap.get(element); + for (PseudoElement pseudo : styleMap.pseudoSet(element)) { + if ("marker".equals(pseudo.getName())) { + NodeData pseudoStyle = styleMap.get(element, pseudo); + if (pseudoStyle.getProperty("content", false) == Content.content_list) { + List> content = pseudoStyle.getValue(TermList.class, "content", false); + if (content != null) + return content; + } + return Collections.emptyList(); + } + } + return null; + } + @Override + protected CounterStyle getListStyleType(Element element, CounterStyle parentListStyleType) { + NodeData style = styleMap.get(element); + ListStyleType type = style.getProperty("list-style-type", false); + if (type == null) + return parentListStyleType; // value is inherited from parent + else { + switch (type) { + case INHERIT: + // should not happen + return parentListStyleType; + case INITIAL: + return CounterStyle.DISC; // see https://www.w3.org/TR/css-lists-3/#propdef-list-style-type + case counter_style_name: + return getNamedCounterStyle(style.getValue(TermIdent.class, "list-style-type").getValue()); + case symbols_fn: + return CounterStyle.fromSymbolsFunction(style.getValue(TermFunction.class, "list-style-type")); + case braille_string: // note that braille CSS does not support any string, as standard CSS does + return null; + case DECIMAL: + return CounterStyle.DECIMAL; + case LOWER_ALPHA: + return CounterStyle.LOWER_ALPHA; + case LOWER_ROMAN: + return CounterStyle.LOWER_ROMAN; + case UPPER_ALPHA: + return CounterStyle.UPPER_ALPHA; + case UPPER_ROMAN: + return CounterStyle.UPPER_ROMAN; + case NONE: + default: + return null; + } + } + } + @Override + protected CounterStyle getNamedCounterStyle(String name) { + if (namedCounterStyles == null) + namedCounterStyles = CounterStyle.parseCounterStyleRules( + Iterables.filter(getParsedStyleSheet(), RuleCounterStyle.class)); + return namedCounterStyles.getOrDefault(name, CounterStyle.DECIMAL); + } + }; XMLStreamWriterHelper.writeStartElement(writer, element); copyAttributes(element, writer); - Map pseudoStyles = new HashMap<>(); { - for (PseudoElement pseudo : styleMap.pseudoSet(element)) - pseudoStyles.put(pseudo, styleMap.get(element, pseudo)); } - NodeData mainStyle = styleMap.get(element); - updateCounterValues(mainStyle); - CounterStyle listCounterStyle = getCurrentListCounterStyle(mainStyle); - String marker = generateMarkerContents(mainStyle, pseudoStyles, element, listCounterStyle); + evaluator.startElement(element); + String marker = evaluator.generateMarkerContents(element); if (marker != null) { if (markerContentAttributeName != null) // if attribute namespace specified, insert as "marker-content" attribute @@ -161,309 +273,12 @@ else if (markerAttributeName != null) // if no attribute specified, insert as text writer.writeCharacters(marker); } - this.listCounterStyle.push(listCounterStyle); - this.counterValues.push(null); for (Node child = element.getFirstChild(); child != null; child = child.getNextSibling()) traverse(child, styleMap, writer); - this.listCounterStyle.pop(); - this.counterValues.pop(); + evaluator.endElement(); writer.writeEndElement(); } - /** - * Determine counter values (see https://www.w3.org/TR/css-lists-3/#creating-counters) - */ - private void updateCounterValues(NodeData style) { - if (style.getProperty("counter-reset", false) == CounterReset.list_values) { - List> list = style.getValue(TermList.class, "counter-reset", false); - if (list != null) { - for (Term t : list) { - if (!(t instanceof TermPair)) - throw new IllegalStateException("coding error"); - TermPair p = (TermPair)t; - String name = p.getKey(); - Map createdBySiblingOrSelf = counterValues.peek(); - if (createdBySiblingOrSelf == null) { - createdBySiblingOrSelf = new HashMap<>(); - counterValues.set(0, createdBySiblingOrSelf); - } - createdBySiblingOrSelf.put(name, p.getValue()); - } - } - } - // counter-set or counter-increment work in addition to counter-reset - Set done = null; - if (style.getProperty("counter-set", false) == CounterSet.list_values) { - List> list = style.getValue(TermList.class, "counter-set", false); - if (list != null) { - for (Term t : list) { - if (!(t instanceof TermPair)) - throw new IllegalStateException("coding error"); - TermPair p = (TermPair)t; - String name = p.getKey(); - boolean exists = false; - for (Map c : counterValues) { - if (c != null) { - c.put(name, p.getValue()); - exists = true; - break; - } - } - if (!exists) { - Map createdBySiblingOrSelf = counterValues.peek(); - if (createdBySiblingOrSelf == null) { - createdBySiblingOrSelf = new HashMap<>(); - counterValues.set(0, createdBySiblingOrSelf); - } - createdBySiblingOrSelf.put(name, p.getValue()); - } - if (done == null) - done = new HashSet<>(); - done.add(name); - } - } - } - if (style.getProperty("counter-increment", false) == CounterIncrement.list_values) { - List> list = style.getValue(TermList.class, "counter-increment", false); - if (list != null) { - for (Term t : list) { - if (!(t instanceof TermPair)) - throw new IllegalStateException("coding error"); - TermPair p = (TermPair)t; - String name = p.getKey(); - if (done == null || !done.contains(name)) { - boolean exists = false; - for (Map c : counterValues) { - if (c != null) { - c.put(name, c.getOrDefault(name, 0) + p.getValue()); - exists = true; - break; - } - } - if (!exists) { - Map createdBySiblingOrSelf = counterValues.peek(); - if (createdBySiblingOrSelf == null) { - createdBySiblingOrSelf = new HashMap<>(); - counterValues.set(0, createdBySiblingOrSelf); - } - createdBySiblingOrSelf.put(name, p.getValue()); - } - if (done == null) - done = new HashSet<>(); - done.add(name); - } - } - } - } - // by default list items increment the 'list-item' counter - if ((done == null || !done.contains("list-item")) && style.getProperty("display", false) == Display.LIST_ITEM) { - boolean exists = false; - for (Map c : counterValues) { - if (c != null) { - c.put("list-item", c.getOrDefault("list-item", 0) + 1); - exists = true; - break; - } - } - if (!exists) { - Map createdBySiblingOrSelf = counterValues.peek(); - if (createdBySiblingOrSelf == null) { - createdBySiblingOrSelf = new HashMap<>(); - counterValues.set(0, createdBySiblingOrSelf); - } - createdBySiblingOrSelf.put("list-item", 1); - } - } - } - - private CounterStyle getCurrentListCounterStyle(NodeData style) { - ListStyleType type = style.getProperty("list-style-type", false); - if (type == null) - return this.listCounterStyle.peek(); // value is inherited from parent - else { - switch (type) { - case INHERIT: - // should not happen - return this.listCounterStyle.peek(); - case INITIAL: - return CounterStyle.DISC; // see https://www.w3.org/TR/css-lists-3/#propdef-list-style-type - case counter_style_name: - return getNamedCounterStyle(style.getValue(TermIdent.class, "list-style-type").getValue()); - case symbols_fn: - return CounterStyle.fromSymbolsFunction(style.getValue(TermFunction.class, "list-style-type")); - case braille_string: // note that braille CSS does not support any string, as standard CSS does - return null; - case DECIMAL: - return CounterStyle.DECIMAL; - case LOWER_ALPHA: - return CounterStyle.LOWER_ALPHA; - case LOWER_ROMAN: - return CounterStyle.LOWER_ROMAN; - case UPPER_ALPHA: - return CounterStyle.UPPER_ALPHA; - case UPPER_ROMAN: - return CounterStyle.UPPER_ROMAN; - case NONE: - default: - return null; - } - } - } - - private CounterStyle getNamedCounterStyle(String name) { - if (namedCounterStyles == null) - namedCounterStyles = CounterStyle.parseCounterStyleRules( - Iterables.filter(getParsedStyleSheet(), RuleCounterStyle.class)); - return namedCounterStyles.getOrDefault(name, CounterStyle.DECIMAL); - } - - private String evaluateCounter(String name, CounterStyle style, boolean withPrefixAndSuffix) { - Integer value = null; - boolean exists = false; - for (Map c : counterValues) - if (c != null && c.containsKey(name)) { - value = c.get(name); - break; - } - if (value == null) { - value = 0; - Map createdBySiblingOrSelf = counterValues.peek(); - if (createdBySiblingOrSelf == null) { - createdBySiblingOrSelf = new HashMap<>(); - counterValues.set(0, createdBySiblingOrSelf); - } - createdBySiblingOrSelf.put(name, value); - } - return style.format(value, withPrefixAndSuffix); - } - - private String evaluateCounters(String name, CounterStyle style, String separator) { - StringBuilder s = new StringBuilder(); - for (Map c : counterValues) - if (c != null && c.containsKey(name)) { - if (s.length() > 0) - s.insert(0, separator); - s.insert(0, style.format(c.get(name))); - } - if (s.length() == 0) { - Map createdBySiblingOrSelf = counterValues.peek(); - if (createdBySiblingOrSelf == null) { - createdBySiblingOrSelf = new HashMap<>(); - counterValues.set(0, createdBySiblingOrSelf); - } - createdBySiblingOrSelf.put(name, 0); - s.append(style.format(0)); - } - return s.toString(); - } - - private String evaluateContent(List> content, Element element) throws IllegalArgumentException { - StringBuilder s = new StringBuilder(); - for (Term t : content) { - if (t instanceof TermString) { - s.append(((TermString)t).getValue()); - } else if (t instanceof TermFunction) { - TermFunction f = (TermFunction)t; - if ("counter".equalsIgnoreCase(f.getFunctionName())) { - String name = null; - CounterStyle style = null; - for (Term arg : f) - if (name == null) - if (arg instanceof TermIdent) - name = ((TermIdent)arg).getValue(); - else - throw new IllegalArgumentException( - "invalid first argument of counter() function: should be the counter name"); - else if (style == null) { - if (arg instanceof TermIdent) - style = getNamedCounterStyle(((TermIdent)arg).getValue()); - else if (arg instanceof TermFunction) - try { - style = CounterStyle.fromSymbolsFunction((TermFunction)arg); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException( - "invalid second argument of counter() function: should be a counter style", e); - } - else - throw new IllegalArgumentException( - "invalid second argument of counter() function: should be a counter style"); } - else - throw new IllegalArgumentException( - "unexpected argument of counter() function: function takes at most two arguments"); - if (name == null) - throw new IllegalArgumentException("counter() function requires at least one argument"); - if (style == null) - style = CounterStyle.DECIMAL; - s.append(evaluateCounter(name, style, false)); - } else if ("counters".equalsIgnoreCase(f.getFunctionName())) { - String name = null; - String separator = null; - CounterStyle style = null; - for (Term arg : f) - if (name == null) - if (arg instanceof TermIdent) - name = ((TermIdent)arg).getValue(); - else - throw new IllegalArgumentException( - "invalid first argument of counters() function: should be the counter name"); - else if (separator == null) { - if (arg instanceof TermString) - separator = ((TermString)arg).getValue(); - else - throw new IllegalArgumentException( - "invalid second argument of counters() function: should be a string"); } - else if (style == null) { - if (arg instanceof TermIdent) - style = getNamedCounterStyle(((TermIdent)arg).getValue()); - else if (arg instanceof TermFunction) - try { - style = CounterStyle.fromSymbolsFunction((TermFunction)arg); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException( - "invalid third argument of counters() function: should be a counter style", e); - } - else - throw new IllegalArgumentException( - "invalid third argument of counters() function: should be a counter style"); } - else - throw new IllegalArgumentException( - "unexpected argument of counters() function: function takes at most three arguments"); - if (name == null || separator == null) - throw new IllegalArgumentException("counters() function requires at least two arguments"); - if (style == null) - style = CounterStyle.DECIMAL; - s.append(evaluateCounters(name, style, separator)); - } else - throw new RuntimeException(f.getFunctionName() + "() function not supported in content list"); // FIXME - } else - throw new IllegalStateException(); // cannot happen - } - return s.toString(); - } - - /** - * Evaluate marker contents (see https://www.w3.org/TR/css-lists-3/#content-property) - */ - private String generateMarkerContents(NodeData mainStyle, Map pseudoStyles, Element element, - CounterStyle listCounterStyle) throws IllegalArgumentException { - if (mainStyle.getProperty("display", false) == Display.LIST_ITEM) { - for (PseudoElement pseudo : pseudoStyles.keySet()) { - if ("marker".equals(pseudo.getName())) { - NodeData pseudoStyle = pseudoStyles.get(pseudo); - if (pseudoStyle.getProperty("content", false) == Content.content_list) { - List> content = pseudoStyle.getValue(TermList.class, "content", false); - if (content != null) - return evaluateContent(content, element); - } - return null; - } - } - if (listCounterStyle != null) - return evaluateCounter("list-item", listCounterStyle, true); - } - return null; - } - protected Map serializeStyle(NodeData mainStyle, Map pseudoStyles, Element context) { throw new UnsupportedOperationException(); // not needed } diff --git a/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/sass/impl/SassAnalyzer.java b/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/sass/impl/SassAnalyzer.java index 26b207a580..0cce94889e 100644 --- a/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/sass/impl/SassAnalyzer.java +++ b/modules/scripts-utils/css-utils/src/main/java/org/daisy/pipeline/css/sass/impl/SassAnalyzer.java @@ -186,7 +186,7 @@ public Collection getVariableDeclarations(Iterable userAnd Reader r = ss.getReader(); if (r == null) r = new InputStreamReader(ss.getInputStream(), StandardCharsets.UTF_8); - stylesheets.add(new CSSSource(CharStreams.toString(r), null, base, 0, 0)); + stylesheets.add(new CSSSource(CharStreams.toString(r), (String)null, base, 0, 0)); } else { if (base == null) { InputSource is = SAXSource.sourceToInputSource(s); @@ -200,7 +200,7 @@ public Collection getVariableDeclarations(Iterable userAnd else throw new IllegalArgumentException("unexpected source: no content and no base URI"); } - stylesheets.add(new CSSSource(CharStreams.toString(r), null, base, 0, 0)); + stylesheets.add(new CSSSource(CharStreams.toString(r), (String)null, base, 0, 0)); } else stylesheets.add(new CSSSource(base, StandardCharsets.UTF_8, null)); } diff --git a/modules/scripts-utils/css-utils/src/main/resources/META-INF/catalog.xml b/modules/scripts-utils/css-utils/src/main/resources/META-INF/catalog.xml index 6d7d0449fa..82bf6aecba 100755 --- a/modules/scripts-utils/css-utils/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts-utils/css-utils/src/main/resources/META-INF/catalog.xml @@ -4,8 +4,4 @@ - - - - diff --git a/modules/scripts-utils/css-utils/src/main/resources/xml/css-cascade.xpl b/modules/scripts-utils/css-utils/src/main/resources/xml/css-cascade.xpl index 33e6e2ff65..389f9d4402 100644 --- a/modules/scripts-utils/css-utils/src/main/resources/xml/css-cascade.xpl +++ b/modules/scripts-utils/css-utils/src/main/resources/xml/css-cascade.xpl @@ -17,7 +17,7 @@ CSS and SCSS style sheets in XML.

Inlining is done with style attributes with the syntax described in - braille CSS.

+ braille CSS. Note that concretizing "inherit" is not part of cascading.

diff --git a/modules/scripts-utils/css-utils/src/test/java/org/daisy/pipeline/css/CounterStyleTest.java b/modules/scripts-utils/css-utils/src/test/java/org/daisy/pipeline/css/CounterStyleTest.java index 06bc5bbdb8..87c13508c2 100644 --- a/modules/scripts-utils/css-utils/src/test/java/org/daisy/pipeline/css/CounterStyleTest.java +++ b/modules/scripts-utils/css-utils/src/test/java/org/daisy/pipeline/css/CounterStyleTest.java @@ -1,6 +1,9 @@ package org.daisy.pipeline.css; import java.util.ArrayList; +import java.util.List; + +import org.daisy.pipeline.css.CounterStyle.AdditiveTuple; import org.junit.Assert; import org.junit.Test; @@ -9,7 +12,7 @@ public class CounterStyleTest { @Test public void testCounterRepresentation() { - ArrayList symbols = new ArrayList(); + List symbols = new ArrayList(); symbols.add("a"); symbols.add("b"); symbols.add("c"); @@ -23,38 +26,61 @@ public void testCounterRepresentation() { Assert.assertEquals("b", CounterStyle.counterRepresentationAlphabetic(2, symbols).orElse("")); Assert.assertEquals("add", CounterStyle.counterRepresentationAlphabetic(100, symbols).orElse("")); Assert.assertEquals("", CounterStyle.counterRepresentationAlphabetic(-1, symbols).orElse("")); - Assert.assertEquals("h", CounterStyle.counterRepresentationCyclic(0 , symbols)); - Assert.assertEquals("a", CounterStyle.counterRepresentationCyclic(1 , symbols)); - Assert.assertEquals("b", CounterStyle.counterRepresentationCyclic(2 , symbols)); - Assert.assertEquals("d", CounterStyle.counterRepresentationCyclic(100 , symbols)); - Assert.assertEquals("g", CounterStyle.counterRepresentationCyclic(-1 , symbols)); - Assert.assertEquals("", CounterStyle.counterRepresentationFixed(0 , symbols).orElse("")); - Assert.assertEquals("a", CounterStyle.counterRepresentationFixed(1 , symbols).orElse("")); - Assert.assertEquals("b", CounterStyle.counterRepresentationFixed(2 , symbols).orElse("")); - Assert.assertEquals("", CounterStyle.counterRepresentationFixed(100 , symbols).orElse("")); - Assert.assertEquals("", CounterStyle.counterRepresentationFixed(-1 , symbols).orElse("")); - Assert.assertEquals("a", CounterStyle.counterRepresentationNumeric(0 , symbols, "-")); - Assert.assertEquals("b", CounterStyle.counterRepresentationNumeric(1 , symbols, "-")); - Assert.assertEquals("c", CounterStyle.counterRepresentationNumeric(2 , symbols, "-")); - Assert.assertEquals("bee", CounterStyle.counterRepresentationNumeric(100 , symbols, "-")); - Assert.assertEquals("-b", CounterStyle.counterRepresentationNumeric(-1 , symbols, "-")); - Assert.assertEquals("", CounterStyle.counterRepresentationSymbolic(0 , symbols).orElse("")); - Assert.assertEquals("a", CounterStyle.counterRepresentationSymbolic(1 , symbols).orElse("")); - Assert.assertEquals("b", CounterStyle.counterRepresentationSymbolic(2 , symbols).orElse("")); - Assert.assertEquals("ddddddddddddd", CounterStyle.counterRepresentationSymbolic(100 , symbols).orElse("")); - Assert.assertEquals("", CounterStyle.counterRepresentationSymbolic(-1 , symbols).orElse("")); + Assert.assertEquals("h", CounterStyle.counterRepresentationCyclic(0, symbols)); + Assert.assertEquals("a", CounterStyle.counterRepresentationCyclic(1, symbols)); + Assert.assertEquals("b", CounterStyle.counterRepresentationCyclic(2, symbols)); + Assert.assertEquals("d", CounterStyle.counterRepresentationCyclic(100, symbols)); + Assert.assertEquals("g", CounterStyle.counterRepresentationCyclic(-1, symbols)); + Assert.assertEquals("", CounterStyle.counterRepresentationFixed(0, symbols).orElse("")); + Assert.assertEquals("a", CounterStyle.counterRepresentationFixed(1, symbols).orElse("")); + Assert.assertEquals("b", CounterStyle.counterRepresentationFixed(2, symbols).orElse("")); + Assert.assertEquals("", CounterStyle.counterRepresentationFixed(100, symbols).orElse("")); + Assert.assertEquals("", CounterStyle.counterRepresentationFixed(-1, symbols).orElse("")); + Assert.assertEquals("a", CounterStyle.counterRepresentationNumeric(0, symbols, "-")); + Assert.assertEquals("b", CounterStyle.counterRepresentationNumeric(1, symbols, "-")); + Assert.assertEquals("c", CounterStyle.counterRepresentationNumeric(2, symbols, "-")); + Assert.assertEquals("bee", CounterStyle.counterRepresentationNumeric(100, symbols, "-")); + Assert.assertEquals("-b", CounterStyle.counterRepresentationNumeric(-1, symbols, "-")); + Assert.assertEquals("", CounterStyle.counterRepresentationSymbolic(0, symbols).orElse("")); + Assert.assertEquals("a", CounterStyle.counterRepresentationSymbolic(1, symbols).orElse("")); + Assert.assertEquals("b", CounterStyle.counterRepresentationSymbolic(2, symbols).orElse("")); + Assert.assertEquals("ddddddddddddd", CounterStyle.counterRepresentationSymbolic(100, symbols).orElse("")); + Assert.assertEquals("", CounterStyle.counterRepresentationSymbolic(-1, symbols).orElse("")); + } + + @Test + public void testCounterRepresentationAdditive() { + List symbols = new ArrayList<>(); + symbols.add(new AdditiveTuple(1000, "⠍")); + symbols.add(new AdditiveTuple(900, "⠉⠍")); + symbols.add(new AdditiveTuple(500, "⠙")); + symbols.add(new AdditiveTuple(400, "⠉⠙")); + symbols.add(new AdditiveTuple(100, "⠉")); + symbols.add(new AdditiveTuple(90, "⠭⠉")); + symbols.add(new AdditiveTuple(50, "⠇")); + symbols.add(new AdditiveTuple(40, "⠭⠇")); + symbols.add(new AdditiveTuple(10, "⠭")); + symbols.add(new AdditiveTuple(9, "⠊⠭")); + symbols.add(new AdditiveTuple(5, "⠧")); + symbols.add(new AdditiveTuple(4, "⠊⠧")); + symbols.add(new AdditiveTuple(1, "⠊")); + Assert.assertEquals("⠍⠉⠍⠭⠉⠧⠊⠊⠊", CounterStyle.counterRepresentationAdditive(1998, symbols).orElse("")); } @Test public void testPredefinedCounterStyles() { + Assert.assertEquals("100", CounterStyle.DECIMAL.format(100)); + Assert.assertEquals("-25", CounterStyle.DECIMAL.format(-25)); Assert.assertEquals("a", CounterStyle.LOWER_ALPHA.format(1)); Assert.assertEquals("j", CounterStyle.LOWER_ALPHA.format(10)); Assert.assertEquals("aa", CounterStyle.LOWER_ALPHA.format(27)); + Assert.assertEquals("cv", CounterStyle.LOWER_ALPHA.format(100)); Assert.assertEquals("0", CounterStyle.LOWER_ALPHA.format(0)); Assert.assertEquals("-1", CounterStyle.LOWER_ALPHA.format(-1)); Assert.assertEquals("i", CounterStyle.LOWER_ROMAN.format(1)); Assert.assertEquals("xcix", CounterStyle.LOWER_ROMAN.format(99)); Assert.assertEquals("0", CounterStyle.LOWER_ROMAN.format(0)); Assert.assertEquals("-1", CounterStyle.LOWER_ROMAN.format(-1)); + Assert.assertEquals("MCMXCVIII", CounterStyle.UPPER_ROMAN.format(1998)); } } diff --git a/modules/scripts-utils/daisy202-utils/pom.xml b/modules/scripts-utils/daisy202-utils/pom.xml index d3e49e47d8..5c84084702 100644 --- a/modules/scripts-utils/daisy202-utils/pom.xml +++ b/modules/scripts-utils/daisy202-utils/pom.xml @@ -17,48 +17,37 @@ DAISY Pipeline 2 module :: DAISY 2.02 Utils - org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules html-utils - runtime org.daisy.pipeline.modules smil-utils - runtime org.daisy.pipeline.modules audio-common - runtime org.daisy.pipeline.modules mediatype-utils - runtime diff --git a/modules/scripts-utils/daisy202-utils/src/main/resources/META-INF/catalog.xml b/modules/scripts-utils/daisy202-utils/src/main/resources/META-INF/catalog.xml index 312a136893..ecbc20054b 100755 --- a/modules/scripts-utils/daisy202-utils/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts-utils/daisy202-utils/src/main/resources/META-INF/catalog.xml @@ -9,7 +9,7 @@ - + @@ -26,12 +26,4 @@ - - - - - - - - diff --git a/modules/scripts-utils/daisy3-utils/pom.xml b/modules/scripts-utils/daisy3-utils/pom.xml index 6d3ea49d04..f433fe3ca2 100644 --- a/modules/scripts-utils/daisy3-utils/pom.xml +++ b/modules/scripts-utils/daisy3-utils/pom.xml @@ -17,43 +17,33 @@ DAISY Pipeline 2 module :: DAISY 3 Utils - org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules smil-utils - runtime org.daisy.pipeline.modules dtbook-utils - runtime org.daisy.pipeline.modules audio-common - runtime diff --git a/modules/scripts-utils/daisy3-utils/src/main/resources/META-INF/catalog.xml b/modules/scripts-utils/daisy3-utils/src/main/resources/META-INF/catalog.xml index cf29889e5c..9bbe8bd018 100755 --- a/modules/scripts-utils/daisy3-utils/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts-utils/daisy3-utils/src/main/resources/META-INF/catalog.xml @@ -29,8 +29,4 @@ - - - - diff --git a/modules/scripts-utils/daisy3-utils/src/test/java/XProcSpecTest.java b/modules/scripts-utils/daisy3-utils/src/test/java/XProcSpecTest.java index 4a5e497f37..eae9bf5d3f 100644 --- a/modules/scripts-utils/daisy3-utils/src/test/java/XProcSpecTest.java +++ b/modules/scripts-utils/daisy3-utils/src/test/java/XProcSpecTest.java @@ -9,6 +9,8 @@ protected String[] testDependencies() { pipelineModule("file-utils"), pipelineModule("fileset-utils"), pipelineModule("smil-utils"), + pipelineModule("dtbook-utils"), + pipelineModule("audio-common"), }; } } diff --git a/modules/scripts-utils/dtbook-utils/pom.xml b/modules/scripts-utils/dtbook-utils/pom.xml index 1ea7376383..f8b57f1270 100644 --- a/modules/scripts-utils/dtbook-utils/pom.xml +++ b/modules/scripts-utils/dtbook-utils/pom.xml @@ -31,53 +31,41 @@ ds-to-spi-runtime provided
- org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules mediatype-utils - runtime org.daisy.pipeline.modules validation-utils - runtime org.daisy.pipeline.modules css-utils - runtime org.daisy.pipeline.modules metadata-utils - runtime org.daisy.pipeline.modules mathml-utils - runtime org.daisy.pipeline.modules dtbook-break-detection - runtime org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules html-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules smil-utils - runtime org.daisy.pipeline.modules mediatype-utils - runtime org.daisy.pipeline.modules zip-utils - runtime org.daisy.pipeline.modules odf-utils - runtime org.daisy.pipeline.modules daisy3-utils - runtime org.daisy.pipeline.modules validation-utils - runtime org.daisy.pipeline.modules epubcheck-adapter - runtime org.daisy.pipeline.modules ace-adapter - runtime
diff --git a/modules/scripts-utils/epub-utils/src/main/resources/META-INF/catalog.xml b/modules/scripts-utils/epub-utils/src/main/resources/META-INF/catalog.xml index ad1a6ac5ba..1f4b1f8295 100755 --- a/modules/scripts-utils/epub-utils/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts-utils/epub-utils/src/main/resources/META-INF/catalog.xml @@ -2,16 +2,4 @@ - - - - - - - - - - - - diff --git a/modules/scripts-utils/epub3-to-html/pom.xml b/modules/scripts-utils/epub3-to-html/pom.xml index c945592f7b..ab83118b34 100644 --- a/modules/scripts-utils/epub3-to-html/pom.xml +++ b/modules/scripts-utils/epub3-to-html/pom.xml @@ -23,13 +23,9 @@ - org.daisy.pipeline.modules epub-utils - runtime - + @@ -71,7 +71,7 @@ - + @@ -116,12 +116,4 @@ - - - - - - - - diff --git a/modules/scripts-utils/html-utils/src/main/resources/xml/dtd/rdfa/xhtml-metaAttributes-2.mod b/modules/scripts-utils/html-utils/src/main/resources/xml/dtd/rdfa11/xhtml-metaAttributes-2.mod similarity index 100% rename from modules/scripts-utils/html-utils/src/main/resources/xml/dtd/rdfa/xhtml-metaAttributes-2.mod rename to modules/scripts-utils/html-utils/src/main/resources/xml/dtd/rdfa11/xhtml-metaAttributes-2.mod diff --git a/modules/scripts-utils/html-utils/src/main/resources/xml/dtd/rdfa/xhtml-rdfa-2.dtd b/modules/scripts-utils/html-utils/src/main/resources/xml/dtd/rdfa11/xhtml-rdfa-2.dtd similarity index 100% rename from modules/scripts-utils/html-utils/src/main/resources/xml/dtd/rdfa/xhtml-rdfa-2.dtd rename to modules/scripts-utils/html-utils/src/main/resources/xml/dtd/rdfa11/xhtml-rdfa-2.dtd diff --git a/modules/scripts-utils/html-utils/src/main/resources/xml/dtd/rdfa/xhtml-rdfa-model-2.mod b/modules/scripts-utils/html-utils/src/main/resources/xml/dtd/rdfa11/xhtml-rdfa-model-2.mod similarity index 100% rename from modules/scripts-utils/html-utils/src/main/resources/xml/dtd/rdfa/xhtml-rdfa-model-2.mod rename to modules/scripts-utils/html-utils/src/main/resources/xml/dtd/rdfa11/xhtml-rdfa-model-2.mod diff --git a/modules/scripts-utils/html-utils/src/main/resources/xml/xslt/html5-normalize-sections-headings.xsl b/modules/scripts-utils/html-utils/src/main/resources/xml/xslt/html5-normalize-sections-headings.xsl index 484901306d..9b594c8358 100644 --- a/modules/scripts-utils/html-utils/src/main/resources/xml/xslt/html5-normalize-sections-headings.xsl +++ b/modules/scripts-utils/html-utils/src/main/resources/xml/xslt/html5-normalize-sections-headings.xsl @@ -14,7 +14,7 @@ - + - org.daisy.pipeline.modules - validation-utils - runtime - - - org.daisy.libs - jing - runtime + org.daisy.pipeline + calabash-adapter + test - + org.daisy.pipeline.modules.impl.Module_metadata_utils - - \ No newline at end of file + + diff --git a/modules/scripts-utils/metadata-utils/src/main/resources/META-INF/catalog.xml b/modules/scripts-utils/metadata-utils/src/main/resources/META-INF/catalog.xml index a45fcfc37e..d643a672df 100755 --- a/modules/scripts-utils/metadata-utils/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts-utils/metadata-utils/src/main/resources/META-INF/catalog.xml @@ -1,5 +1,4 @@ - diff --git a/modules/scripts-utils/metadata-utils/src/test/java/XProcSpecTest.java b/modules/scripts-utils/metadata-utils/src/test/java/XProcSpecTest.java index c2a5718199..8c7e052fed 100644 --- a/modules/scripts-utils/metadata-utils/src/test/java/XProcSpecTest.java +++ b/modules/scripts-utils/metadata-utils/src/test/java/XProcSpecTest.java @@ -1,13 +1,4 @@ import org.daisy.pipeline.junit.AbstractXSpecAndXProcSpecTest; public class XProcSpecTest extends AbstractXSpecAndXProcSpecTest { - - @Override - protected String[] testDependencies() { - return new String[] { - "org.daisy.libs:jing:?", - pipelineModule("common-utils"), - pipelineModule("validation-utils"), - }; - } } diff --git a/modules/scripts-utils/odf-utils/pom.xml b/modules/scripts-utils/odf-utils/pom.xml index 0d0615d998..b92d9ce831 100644 --- a/modules/scripts-utils/odf-utils/pom.xml +++ b/modules/scripts-utils/odf-utils/pom.xml @@ -21,23 +21,17 @@ org.daisy.pipeline calabash-adapter - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime diff --git a/modules/scripts-utils/odf-utils/src/main/resources/META-INF/catalog.xml b/modules/scripts-utils/odf-utils/src/main/resources/META-INF/catalog.xml index bf1fbdc7b8..d65850fb46 100755 --- a/modules/scripts-utils/odf-utils/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts-utils/odf-utils/src/main/resources/META-INF/catalog.xml @@ -1,7 +1,4 @@ - - - diff --git a/modules/scripts-utils/smil-utils/pom.xml b/modules/scripts-utils/smil-utils/pom.xml index f2cfe5247e..25485e64d5 100644 --- a/modules/scripts-utils/smil-utils/pom.xml +++ b/modules/scripts-utils/smil-utils/pom.xml @@ -17,28 +17,21 @@ DAISY Pipeline 2 module :: SMIL Utilities - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules mediatype-utils - runtime diff --git a/modules/scripts-utils/smil-utils/src/main/resources/META-INF/catalog.xml b/modules/scripts-utils/smil-utils/src/main/resources/META-INF/catalog.xml index c430bffbfb..5da87af82a 100755 --- a/modules/scripts-utils/smil-utils/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts-utils/smil-utils/src/main/resources/META-INF/catalog.xml @@ -12,8 +12,4 @@ - - - - diff --git a/modules/scripts-utils/zedai-utils/pom.xml b/modules/scripts-utils/zedai-utils/pom.xml index ba49455596..5b79c19549 100644 --- a/modules/scripts-utils/zedai-utils/pom.xml +++ b/modules/scripts-utils/zedai-utils/pom.xml @@ -17,33 +17,25 @@ DAISY Pipeline 2 module :: ZedAI Utilities - org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules mathml-utils - runtime org.daisy.pipeline.modules mediatype-utils - runtime org.daisy.pipeline.modules validation-utils - runtime org.daisy.pipeline.modules css-utils - runtime diff --git a/modules/scripts-utils/zedai-utils/src/main/resources/META-INF/catalog.xml b/modules/scripts-utils/zedai-utils/src/main/resources/META-INF/catalog.xml index d278e05181..ae74b20022 100755 --- a/modules/scripts-utils/zedai-utils/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts-utils/zedai-utils/src/main/resources/META-INF/catalog.xml @@ -2,9 +2,4 @@ - - - - - diff --git a/modules/scripts/daisy202-to-daisy3/pom.xml b/modules/scripts/daisy202-to-daisy3/pom.xml index 7a782ef7af..4ba1a9447a 100644 --- a/modules/scripts/daisy202-to-daisy3/pom.xml +++ b/modules/scripts/daisy202-to-daisy3/pom.xml @@ -21,53 +21,41 @@ org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules html-utils - runtime org.daisy.pipeline.modules smil-utils - runtime org.daisy.pipeline.modules daisy202-utils - runtime org.daisy.pipeline.modules daisy3-utils - runtime org.daisy.pipeline.modules dtbook-utils - runtime org.daisy.pipeline.modules html-to-dtbook - runtime diff --git a/modules/scripts/daisy202-to-daisy3/src/main/resources/META-INF/catalog.xml b/modules/scripts/daisy202-to-daisy3/src/main/resources/META-INF/catalog.xml index bf291c2e5c..6d3c6fbd79 100644 --- a/modules/scripts/daisy202-to-daisy3/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts/daisy202-to-daisy3/src/main/resources/META-INF/catalog.xml @@ -1,13 +1,4 @@ - - - - - - - - - diff --git a/modules/scripts/daisy202-to-epub3/pom.xml b/modules/scripts/daisy202-to-epub3/pom.xml index 3e0b6736e9..1c5f2e4f5a 100644 --- a/modules/scripts/daisy202-to-epub3/pom.xml +++ b/modules/scripts/daisy202-to-epub3/pom.xml @@ -17,7 +17,6 @@ DAISY Pipeline 2 module :: DAISY 2.02 to EPUB 3 - org.daisy.pipeline framework-core @@ -28,42 +27,34 @@ org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules daisy202-utils - runtime org.daisy.pipeline.modules epub-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules html-utils - runtime org.daisy.pipeline.modules smil-utils - runtime org.daisy.pipeline.modules mediatype-utils - runtime diff --git a/modules/scripts/daisy202-to-epub3/src/main/resources/META-INF/catalog.xml b/modules/scripts/daisy202-to-epub3/src/main/resources/META-INF/catalog.xml index dabde81c16..906e6151a5 100755 --- a/modules/scripts/daisy202-to-epub3/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts/daisy202-to-epub3/src/main/resources/META-INF/catalog.xml @@ -2,13 +2,4 @@ - - - - - - - - - diff --git a/modules/scripts/daisy202-to-mp3/pom.xml b/modules/scripts/daisy202-to-mp3/pom.xml index 5c1c8e5de8..e535df9109 100644 --- a/modules/scripts/daisy202-to-mp3/pom.xml +++ b/modules/scripts/daisy202-to-mp3/pom.xml @@ -21,38 +21,29 @@ org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules audio-common - runtime org.daisy.pipeline.modules daisy202-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules smil-utils - runtime org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules daisy202-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules smil-utils - runtime org.daisy.pipeline.modules mediatype-utils - runtime org.daisy.pipeline.modules validation-utils - runtime diff --git a/modules/scripts/daisy202-validator/src/main/resources/META-INF/catalog.xml b/modules/scripts/daisy202-validator/src/main/resources/META-INF/catalog.xml index 9654bbb32d..224b0c77e9 100644 --- a/modules/scripts/daisy202-validator/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts/daisy202-validator/src/main/resources/META-INF/catalog.xml @@ -4,11 +4,4 @@ - - - - - - - diff --git a/modules/scripts/daisy3-to-daisy202/pom.xml b/modules/scripts/daisy3-to-daisy202/pom.xml index 7d26996ea0..4b1d448e86 100644 --- a/modules/scripts/daisy3-to-daisy202/pom.xml +++ b/modules/scripts/daisy3-to-daisy202/pom.xml @@ -17,48 +17,37 @@ DAISY Pipeline 2 module :: DAISY 3 to DAISY 2.02 - org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules daisy3-utils - runtime org.daisy.pipeline.modules daisy202-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules smil-utils - runtime org.daisy.pipeline.modules mediatype-utils - runtime diff --git a/modules/scripts/daisy3-to-daisy202/src/main/resources/META-INF/catalog.xml b/modules/scripts/daisy3-to-daisy202/src/main/resources/META-INF/catalog.xml index a8b0870e4a..3ada378f91 100755 --- a/modules/scripts/daisy3-to-daisy202/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts/daisy3-to-daisy202/src/main/resources/META-INF/catalog.xml @@ -6,11 +6,4 @@ px:extends="../../../../../common-options.xpl"/> - - - - - - - diff --git a/modules/scripts/daisy3-to-epub3/pom.xml b/modules/scripts/daisy3-to-epub3/pom.xml index 64afc47503..ded00cad05 100644 --- a/modules/scripts/daisy3-to-epub3/pom.xml +++ b/modules/scripts/daisy3-to-epub3/pom.xml @@ -17,53 +17,41 @@ DAISY Pipeline 2 module :: DAISY 3 to EPUB 3 - org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules html-utils - runtime org.daisy.pipeline.modules daisy3-utils - runtime org.daisy.pipeline.modules dtbook-to-html - runtime org.daisy.pipeline.modules epub-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules smil-utils - runtime diff --git a/modules/scripts/daisy3-to-epub3/src/main/resources/META-INF/catalog.xml b/modules/scripts/daisy3-to-epub3/src/main/resources/META-INF/catalog.xml index 786e2daf4a..bf9327b631 100755 --- a/modules/scripts/daisy3-to-epub3/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts/daisy3-to-epub3/src/main/resources/META-INF/catalog.xml @@ -4,12 +4,4 @@ uri="../xml/xproc/daisy3-to-epub3.xpl" px:extends="../../../../../common-options.xpl" px:content-type="script" px:id="daisy3-to-epub3"/> - - - - - - - - diff --git a/modules/scripts/daisy3-to-mp3/pom.xml b/modules/scripts/daisy3-to-mp3/pom.xml index ff51f46e66..d7ceb4b9d7 100644 --- a/modules/scripts/daisy3-to-mp3/pom.xml +++ b/modules/scripts/daisy3-to-mp3/pom.xml @@ -21,38 +21,29 @@ org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules audio-common - runtime org.daisy.pipeline.modules daisy3-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules smil-utils - runtime org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules css-utils - runtime org.daisy.pipeline.modules daisy3-utils - runtime org.daisy.pipeline.modules dtbook-tts - runtime org.daisy.pipeline.modules dtbook-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules smil-utils - runtime - diff --git a/modules/scripts/dtbook-to-epub3/pom.xml b/modules/scripts/dtbook-to-epub3/pom.xml index 3e05d36bfc..70024b153e 100644 --- a/modules/scripts/dtbook-to-epub3/pom.xml +++ b/modules/scripts/dtbook-to-epub3/pom.xml @@ -17,48 +17,37 @@ DAISY Pipeline 2 module :: DTBook to EPUB 3 - org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules css-utils - runtime org.daisy.pipeline.modules dtbook-to-zedai - runtime org.daisy.pipeline.modules dtbook-utils - runtime org.daisy.pipeline.modules epub-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules zedai-to-epub3 - runtime org.daisy.pipeline.modules @@ -73,11 +62,6 @@ nlp-omnilang-lexer test - - org.daisy.pipeline.modules - audio-common - test - org.daisy.pipeline.modules tts-mocks @@ -93,4 +77,4 @@ - \ No newline at end of file + diff --git a/modules/scripts/dtbook-to-epub3/src/main/resources/META-INF/catalog.xml b/modules/scripts/dtbook-to-epub3/src/main/resources/META-INF/catalog.xml index fabe1d798f..f6894ebc41 100755 --- a/modules/scripts/dtbook-to-epub3/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts/dtbook-to-epub3/src/main/resources/META-INF/catalog.xml @@ -3,11 +3,4 @@ - - - - - - - diff --git a/modules/scripts/dtbook-to-html/pom.xml b/modules/scripts/dtbook-to-html/pom.xml index 8e932dd963..b93682201f 100644 --- a/modules/scripts/dtbook-to-html/pom.xml +++ b/modules/scripts/dtbook-to-html/pom.xml @@ -17,43 +17,33 @@ DAISY Pipeline 2 module :: DTBook to HTML - org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules dtbook-to-zedai - runtime org.daisy.pipeline.modules dtbook-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules zedai-to-html - runtime diff --git a/modules/scripts/dtbook-to-html/src/main/resources/META-INF/catalog.xml b/modules/scripts/dtbook-to-html/src/main/resources/META-INF/catalog.xml index dd5707ae19..ddc95e9386 100755 --- a/modules/scripts/dtbook-to-html/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts/dtbook-to-html/src/main/resources/META-INF/catalog.xml @@ -4,10 +4,4 @@ uri="../xml/dtbook-to-html.xpl" px:extends="../../../../../common-options.xpl" px:content-type="script" px:id="dtbook-to-html"/> - - - - - - diff --git a/modules/scripts/dtbook-to-odt/pom.xml b/modules/scripts/dtbook-to-odt/pom.xml index 3de6e0305b..51f31c30fa 100644 --- a/modules/scripts/dtbook-to-odt/pom.xml +++ b/modules/scripts/dtbook-to-odt/pom.xml @@ -18,38 +18,29 @@ DAISY Pipeline 2 module :: DTBook to ODT - org.daisy.pipeline framework-core - org.daisy.pipeline.modules asciimath-utils - runtime org.daisy.pipeline.modules dtbook-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules odf-utils - runtime org.daisy.pipeline.modules image-utils - runtime diff --git a/modules/scripts/dtbook-to-odt/src/main/resources/META-INF/catalog.xml b/modules/scripts/dtbook-to-odt/src/main/resources/META-INF/catalog.xml index 8e7329d967..7cf75538d3 100644 --- a/modules/scripts/dtbook-to-odt/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts/dtbook-to-odt/src/main/resources/META-INF/catalog.xml @@ -4,9 +4,4 @@ - - - - - diff --git a/modules/scripts/dtbook-to-pef/pom.xml b/modules/scripts/dtbook-to-pef/pom.xml index 3feb527c32..a20429418b 100644 --- a/modules/scripts/dtbook-to-pef/pom.xml +++ b/modules/scripts/dtbook-to-pef/pom.xml @@ -18,7 +18,6 @@ DAISY Pipeline 2 module :: DTBook to PEF - org.daisy.pipeline framework-core @@ -27,48 +26,37 @@ org.daisy.pipeline.modules css-utils - org.daisy.pipeline.modules.braille braille-common - runtime org.daisy.pipeline.modules.braille pef-utils - runtime org.daisy.pipeline.modules dtbook-utils - runtime org.daisy.pipeline.modules metadata-utils - runtime org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules dtbook-to-epub3 - runtime - - - - - - - - - - diff --git a/modules/scripts/dtbook-to-pef/src/main/resources/xml/xproc/dtbook-to-pef.xpl b/modules/scripts/dtbook-to-pef/src/main/resources/xml/xproc/dtbook-to-pef.xpl index 4ee6439e2c..27c1c25f70 100644 --- a/modules/scripts/dtbook-to-pef/src/main/resources/xml/xproc/dtbook-to-pef.xpl +++ b/modules/scripts/dtbook-to-pef/src/main/resources/xml/xproc/dtbook-to-pef.xpl @@ -110,10 +110,7 @@ sheet modules) are available for use in Sass style sheets: - - + px:dtbook-to-pef px:dtbook-to-pef.store diff --git a/modules/scripts/dtbook-to-rtf/pom.xml b/modules/scripts/dtbook-to-rtf/pom.xml index 040d483a0a..6b5b299d56 100644 --- a/modules/scripts/dtbook-to-rtf/pom.xml +++ b/modules/scripts/dtbook-to-rtf/pom.xml @@ -17,33 +17,25 @@ DAISY Pipeline 2 module :: DTBook to RTF - org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules dtbook-utils - runtime diff --git a/modules/scripts/dtbook-to-rtf/src/main/resources/META-INF/catalog.xml b/modules/scripts/dtbook-to-rtf/src/main/resources/META-INF/catalog.xml index 649ca679d3..5830420faf 100644 --- a/modules/scripts/dtbook-to-rtf/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts/dtbook-to-rtf/src/main/resources/META-INF/catalog.xml @@ -3,8 +3,4 @@ - - - - diff --git a/modules/scripts/dtbook-to-zedai/pom.xml b/modules/scripts/dtbook-to-zedai/pom.xml index 9656e2c4eb..44c66b0848 100644 --- a/modules/scripts/dtbook-to-zedai/pom.xml +++ b/modules/scripts/dtbook-to-zedai/pom.xml @@ -17,43 +17,33 @@ DAISY Pipeline 2 module :: DTBook to ZedAI - org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules dtbook-utils - runtime org.daisy.pipeline.modules zedai-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules mediatype-utils - runtime diff --git a/modules/scripts/dtbook-to-zedai/src/main/resources/META-INF/catalog.xml b/modules/scripts/dtbook-to-zedai/src/main/resources/META-INF/catalog.xml index 04f2287065..3d56313f09 100755 --- a/modules/scripts/dtbook-to-zedai/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts/dtbook-to-zedai/src/main/resources/META-INF/catalog.xml @@ -3,10 +3,4 @@ - - - - - - diff --git a/modules/scripts/dtbook-validator/pom.xml b/modules/scripts/dtbook-validator/pom.xml index 2415ee4058..6f4c80e6db 100644 --- a/modules/scripts/dtbook-validator/pom.xml +++ b/modules/scripts/dtbook-validator/pom.xml @@ -17,28 +17,21 @@ DAISY Pipeline 2 module :: DTBook Validator - org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules dtbook-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime diff --git a/modules/scripts/dtbook-validator/src/main/resources/META-INF/catalog.xml b/modules/scripts/dtbook-validator/src/main/resources/META-INF/catalog.xml index 3d319acac0..e27d4a8e79 100755 --- a/modules/scripts/dtbook-validator/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts/dtbook-validator/src/main/resources/META-INF/catalog.xml @@ -2,7 +2,4 @@ - - - diff --git a/modules/scripts/epub-to-daisy/pom.xml b/modules/scripts/epub-to-daisy/pom.xml index 6b9b6576d2..6a99b10bab 100644 --- a/modules/scripts/epub-to-daisy/pom.xml +++ b/modules/scripts/epub-to-daisy/pom.xml @@ -22,43 +22,33 @@ org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules epub-utils - runtime org.daisy.pipeline.modules epub2-to-epub3 - runtime org.daisy.pipeline.modules epub3-to-epub3 - runtime org.daisy.pipeline.modules epub3-to-daisy202 - runtime org.daisy.pipeline.modules epub3-to-daisy3 - runtime org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules html-utils - runtime org.daisy.pipeline.modules epub-utils - runtime diff --git a/modules/scripts/epub2-to-epub3/src/main/resources/META-INF/catalog.xml b/modules/scripts/epub2-to-epub3/src/main/resources/META-INF/catalog.xml index 07a2849126..3825114377 100644 --- a/modules/scripts/epub2-to-epub3/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts/epub2-to-epub3/src/main/resources/META-INF/catalog.xml @@ -3,8 +3,4 @@ - - - - diff --git a/modules/scripts/epub3-to-daisy202/pom.xml b/modules/scripts/epub3-to-daisy202/pom.xml index b3e5d20b79..40a07502c2 100644 --- a/modules/scripts/epub3-to-daisy202/pom.xml +++ b/modules/scripts/epub3-to-daisy202/pom.xml @@ -17,53 +17,41 @@ DAISY Pipeline 2 module :: EPUB 3 to DAISY 2.02 - org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules html-utils - runtime org.daisy.pipeline.modules mediatype-utils - runtime org.daisy.pipeline.modules epub-utils - runtime org.daisy.pipeline.modules daisy202-utils - runtime org.daisy.pipeline.modules smil-utils - runtime org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules html-utils - runtime org.daisy.pipeline.modules epub-utils - runtime org.daisy.pipeline.modules dtbook-utils - runtime org.daisy.pipeline.modules daisy3-utils - runtime org.daisy.pipeline.modules smil-utils - runtime org.daisy.pipeline.modules @@ -66,7 +56,6 @@ org.daisy.pipeline.modules epub3-to-html - runtime diff --git a/modules/scripts/epub3-to-daisy3/src/main/resources/META-INF/catalog.xml b/modules/scripts/epub3-to-daisy3/src/main/resources/META-INF/catalog.xml index c80acf1000..51e7d604e4 100755 --- a/modules/scripts/epub3-to-daisy3/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts/epub3-to-daisy3/src/main/resources/META-INF/catalog.xml @@ -5,14 +5,4 @@ px:extends="../../../../../common-options.xpl"/> - - - - - - - - - - diff --git a/modules/scripts/epub3-to-epub3/pom.xml b/modules/scripts/epub3-to-epub3/pom.xml index ff11b75d0b..97ae16d14c 100644 --- a/modules/scripts/epub3-to-epub3/pom.xml +++ b/modules/scripts/epub3-to-epub3/pom.xml @@ -18,68 +18,53 @@ DAISY Pipeline 2 module :: EPUB 3 Enhancer - org.daisy.pipeline framework-core - org.daisy.pipeline.modules css-utils - runtime org.daisy.pipeline.modules.braille braille-css-utils - runtime org.daisy.pipeline.modules.braille braille-common - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules html-utils - runtime org.daisy.pipeline.modules epub-utils - runtime org.daisy.pipeline.modules epub3-tts - runtime org.daisy.pipeline.modules html-break-detection - runtime org.daisy.pipeline.modules tts-common - runtime org.daisy.pipeline framework-core - org.daisy.pipeline.modules.braille html-to-pef - runtime org.daisy.pipeline.modules.braille braille-common - runtime org.daisy.pipeline.modules css-utils - runtime org.daisy.pipeline.modules.braille pef-utils - runtime org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime - - - org.daisy.pipeline.modules - mediatype-utils - runtime org.daisy.pipeline.modules epub-utils - runtime - - - - - - - - - - diff --git a/modules/scripts/epub3-to-pef/src/main/resources/xml/xproc/epub3-to-pef.xpl b/modules/scripts/epub3-to-pef/src/main/resources/xml/xproc/epub3-to-pef.xpl index 3438b643c3..d151835a2a 100644 --- a/modules/scripts/epub3-to-pef/src/main/resources/xml/xproc/epub3-to-pef.xpl +++ b/modules/scripts/epub3-to-pef/src/main/resources/xml/xproc/epub3-to-pef.xpl @@ -107,10 +107,7 @@ even though the provided CSS is more specific. - - + px:epub3-to-pef.load px:epub3-to-pef diff --git a/modules/scripts/epub3-to-pef/src/test/java/XProcSpecTest.java b/modules/scripts/epub3-to-pef/src/test/java/XProcSpecTest.java index e5ec6e17cf..17c54e3110 100644 --- a/modules/scripts/epub3-to-pef/src/test/java/XProcSpecTest.java +++ b/modules/scripts/epub3-to-pef/src/test/java/XProcSpecTest.java @@ -16,7 +16,6 @@ protected String[] testDependencies() { pipelineModule("file-utils"), pipelineModule("fileset-utils"), pipelineModule("common-utils"), - pipelineModule("mediatype-utils"), pipelineModule("epub-utils"), }; } diff --git a/modules/scripts/epub3-validator/pom.xml b/modules/scripts/epub3-validator/pom.xml index 115ad046d9..96129763e2 100644 --- a/modules/scripts/epub3-validator/pom.xml +++ b/modules/scripts/epub3-validator/pom.xml @@ -17,18 +17,13 @@ DAISY Pipeline 2 module :: EPUB 3 Validator - org.daisy.pipeline framework-core - org.daisy.pipeline.modules epub-utils - runtime diff --git a/modules/scripts/epub3-validator/src/main/resources/META-INF/catalog.xml b/modules/scripts/epub3-validator/src/main/resources/META-INF/catalog.xml index 7d790f69d5..b1965fbcd2 100644 --- a/modules/scripts/epub3-validator/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts/epub3-validator/src/main/resources/META-INF/catalog.xml @@ -1,8 +1,4 @@ - - - - diff --git a/modules/scripts/html-to-dtbook/pom.xml b/modules/scripts/html-to-dtbook/pom.xml index 77915f1974..d8973975f7 100644 --- a/modules/scripts/html-to-dtbook/pom.xml +++ b/modules/scripts/html-to-dtbook/pom.xml @@ -17,33 +17,25 @@ DAISY Pipeline 2 module :: HTML to DTBook - org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules epub-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules epub-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules html-utils - runtime org.daisy.pipeline.modules zedai-to-html - runtime org.daisy.pipeline.modules epub3-tts - runtime diff --git a/modules/scripts/html-to-epub3/src/main/resources/META-INF/catalog.xml b/modules/scripts/html-to-epub3/src/main/resources/META-INF/catalog.xml index 153ebf82c4..66afe8214a 100755 --- a/modules/scripts/html-to-epub3/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts/html-to-epub3/src/main/resources/META-INF/catalog.xml @@ -2,11 +2,4 @@ - - - - - - - diff --git a/modules/scripts/html-to-pef/pom.xml b/modules/scripts/html-to-pef/pom.xml index 8b9913cff2..c0f77b8f84 100644 --- a/modules/scripts/html-to-pef/pom.xml +++ b/modules/scripts/html-to-pef/pom.xml @@ -18,7 +18,6 @@ DAISY Pipeline 2 module :: HTML to PEF - org.daisy.pipeline framework-core @@ -27,38 +26,29 @@ org.daisy.pipeline.modules css-utils - org.daisy.pipeline.modules.braille braille-common - runtime org.daisy.pipeline.modules.braille pef-utils - runtime org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules html-utils - runtime org.daisy.pipeline.modules html-to-epub3 - runtime - - - - - - - - diff --git a/modules/scripts/html-to-pef/src/main/resources/xml/xproc/html-to-pef.xpl b/modules/scripts/html-to-pef/src/main/resources/xml/xproc/html-to-pef.xpl index eaaef6ae25..39921e03f9 100644 --- a/modules/scripts/html-to-pef/src/main/resources/xml/xproc/html-to-pef.xpl +++ b/modules/scripts/html-to-pef/src/main/resources/xml/xproc/html-to-pef.xpl @@ -118,10 +118,7 @@ sheet modules) are available for use in Sass style sheets: px:delete-parameters - - + px:html-to-pef px:html-to-pef.store diff --git a/modules/scripts/nimas-fileset-validator/pom.xml b/modules/scripts/nimas-fileset-validator/pom.xml index 6dcfc03766..e0c4d58cea 100644 --- a/modules/scripts/nimas-fileset-validator/pom.xml +++ b/modules/scripts/nimas-fileset-validator/pom.xml @@ -17,33 +17,25 @@ DAISY Pipeline 2 module :: NIMAS Fileset Validator - org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules dtbook-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules validation-utils - runtime - - - - - + + - - - - - - - - - - - - - - - + + + + + + + + + diff --git a/modules/scripts/zedai-to-epub3/pom.xml b/modules/scripts/zedai-to-epub3/pom.xml index b73027f839..5ff7d83462 100644 --- a/modules/scripts/zedai-to-epub3/pom.xml +++ b/modules/scripts/zedai-to-epub3/pom.xml @@ -17,53 +17,41 @@ DAISY Pipeline 2 module :: ZedAI to EPUB 3 - org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules css-utils - runtime org.daisy.pipeline.modules epub-utils - runtime org.daisy.pipeline.modules html-to-epub3 - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules zedai-to-html - runtime org.daisy.pipeline.modules zedai-utils - runtime @@ -74,4 +62,4 @@ - \ No newline at end of file + diff --git a/modules/scripts/zedai-to-epub3/src/main/resources/META-INF/catalog.xml b/modules/scripts/zedai-to-epub3/src/main/resources/META-INF/catalog.xml index 100a9baf04..7ab2eaf6d3 100755 --- a/modules/scripts/zedai-to-epub3/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts/zedai-to-epub3/src/main/resources/META-INF/catalog.xml @@ -3,12 +3,4 @@ - - - - - - - - diff --git a/modules/scripts/zedai-to-html/pom.xml b/modules/scripts/zedai-to-html/pom.xml index 1f4b6b9e50..c2861774fc 100644 --- a/modules/scripts/zedai-to-html/pom.xml +++ b/modules/scripts/zedai-to-html/pom.xml @@ -17,38 +17,29 @@ DAISY Pipeline 2 module :: ZedAI to HTML - org.daisy.pipeline framework-core - org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules html-utils - runtime org.daisy.pipeline.modules zedai-utils - runtime diff --git a/modules/scripts/zedai-to-html/src/main/resources/META-INF/catalog.xml b/modules/scripts/zedai-to-html/src/main/resources/META-INF/catalog.xml index ddd7524c70..8bd8758500 100755 --- a/modules/scripts/zedai-to-html/src/main/resources/META-INF/catalog.xml +++ b/modules/scripts/zedai-to-html/src/main/resources/META-INF/catalog.xml @@ -4,10 +4,4 @@ uri="../xml/xproc/zedai-to-html.xpl" px:content-type="script" px:id="zedai-to-html"/> - - - - - - diff --git a/modules/scripts/zedai-to-pef/pom.xml b/modules/scripts/zedai-to-pef/pom.xml index 84910da3a7..320e1ab270 100644 --- a/modules/scripts/zedai-to-pef/pom.xml +++ b/modules/scripts/zedai-to-pef/pom.xml @@ -18,58 +18,45 @@ DAISY Pipeline 2 module :: ZedAI to PEF - org.daisy.pipeline framework-core - org.daisy.pipeline.modules.braille braille-common - runtime org.daisy.pipeline.modules css-utils - runtime org.daisy.pipeline.modules.braille braille-css-utils - runtime org.daisy.pipeline.modules.braille pef-utils - runtime org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules file-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules zedai-utils - runtime org.daisy.pipeline.modules zedai-to-epub3 - runtime org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules dtbook-break-detection - runtime org.daisy.pipeline.modules tts-common - runtime org.daisy.pipeline.modules mathml-to-ssml - runtime org.daisy.pipeline.modules common-utils - runtime org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules html-break-detection - runtime org.daisy.pipeline.modules tts-common - runtime org.daisy.pipeline.modules mathml-to-ssml - runtime @@ -59,4 +51,4 @@ - \ No newline at end of file + diff --git a/modules/tts/epub3-tts/src/main/resources/META-INF/catalog.xml b/modules/tts/epub3-tts/src/main/resources/META-INF/catalog.xml index bbcb32048b..9027fa8963 100644 --- a/modules/tts/epub3-tts/src/main/resources/META-INF/catalog.xml +++ b/modules/tts/epub3-tts/src/main/resources/META-INF/catalog.xml @@ -6,11 +6,4 @@ px:stylesheet-for-content-type="application/xhtml+xml" px:stylesheet-for-media="speech"/> - - - - - - - diff --git a/modules/tts/mathml-to-ssml/pom.xml b/modules/tts/mathml-to-ssml/pom.xml index 635b026a99..d0083c3ebf 100644 --- a/modules/tts/mathml-to-ssml/pom.xml +++ b/modules/tts/mathml-to-ssml/pom.xml @@ -27,7 +27,6 @@ org.daisy.pipeline.modules common-utils - runtime diff --git a/modules/tts/mathml-to-ssml/src/main/resources/META-INF/catalog.xml b/modules/tts/mathml-to-ssml/src/main/resources/META-INF/catalog.xml index f9c9c47bb1..fa5944040b 100644 --- a/modules/tts/mathml-to-ssml/src/main/resources/META-INF/catalog.xml +++ b/modules/tts/mathml-to-ssml/src/main/resources/META-INF/catalog.xml @@ -1,8 +1,4 @@ - - - - diff --git a/modules/tts/tts-adapters/tts-adapter-sapinative/src/test/java/org/daisy/pipeline/tts/sapi/impl/SapiSSMLTest.java b/modules/tts/tts-adapters/tts-adapter-sapinative/src/test/java/org/daisy/pipeline/tts/sapi/impl/SapiSSMLTest.java index 56aa4a1ad9..12e5399bd0 100644 --- a/modules/tts/tts-adapters/tts-adapter-sapinative/src/test/java/org/daisy/pipeline/tts/sapi/impl/SapiSSMLTest.java +++ b/modules/tts/tts-adapters/tts-adapter-sapinative/src/test/java/org/daisy/pipeline/tts/sapi/impl/SapiSSMLTest.java @@ -84,9 +84,9 @@ public void exampleSSML() throws SaxonApiException, IOException, SAXException { XdmNode toTest = Proc.newDocumentBuilder().build(new StreamSource(new StringReader( "" + "" + - "this" + - "is" + - "a" + + "this " + + "is " + + "a " + "sentence" + "" + "" diff --git a/modules/tts/tts-common/pom.xml b/modules/tts/tts-common/pom.xml index 67e782e176..a6e44971e4 100644 --- a/modules/tts/tts-common/pom.xml +++ b/modules/tts/tts-common/pom.xml @@ -54,9 +54,6 @@ org.daisy.pipeline.modules css-utils - org.daisy.pipeline.modules common-utils @@ -64,12 +61,10 @@ org.daisy.pipeline.modules fileset-utils - runtime org.daisy.pipeline.modules file-utils - runtime + + diff --git a/modules/braille/braille-css-utils/src/main/resources/xml/parse-content.xsl b/modules/braille/braille-css-utils/src/main/resources/xml/parse-content.xsl index ff547f29ba..ff49a8d7fc 100644 --- a/modules/braille/braille-css-utils/src/main/resources/xml/parse-content.xsl +++ b/modules/braille/braille-css-utils/src/main/resources/xml/parse-content.xsl @@ -6,7 +6,7 @@ exclude-result-prefixes="#all" version="2.0"> - + diff --git a/modules/braille/braille-css-utils/src/main/resources/xml/parse-properties.xsl b/modules/braille/braille-css-utils/src/main/resources/xml/parse-properties.xsl index 75a51ef398..08e5629535 100644 --- a/modules/braille/braille-css-utils/src/main/resources/xml/parse-properties.xsl +++ b/modules/braille/braille-css-utils/src/main/resources/xml/parse-properties.xsl @@ -6,7 +6,7 @@ exclude-result-prefixes="#all" version="2.0"> - + diff --git a/modules/braille/braille-css-utils/src/main/resources/xml/parse-stylesheet.xsl b/modules/braille/braille-css-utils/src/main/resources/xml/parse-stylesheet.xsl index 9eec06dc5c..619d01a1b1 100644 --- a/modules/braille/braille-css-utils/src/main/resources/xml/parse-stylesheet.xsl +++ b/modules/braille/braille-css-utils/src/main/resources/xml/parse-stylesheet.xsl @@ -6,7 +6,7 @@ exclude-result-prefixes="#all" version="2.0"> - + diff --git a/modules/braille/dotify-utils/src/main/java/org/daisy/pipeline/braille/dotify/impl/BrailleTranslatorFactoryServiceImpl.java b/modules/braille/dotify-utils/src/main/java/org/daisy/pipeline/braille/dotify/impl/BrailleTranslatorFactoryServiceImpl.java index 955df5768e..49b43c8946 100644 --- a/modules/braille/dotify-utils/src/main/java/org/daisy/pipeline/braille/dotify/impl/BrailleTranslatorFactoryServiceImpl.java +++ b/modules/braille/dotify-utils/src/main/java/org/daisy/pipeline/braille/dotify/impl/BrailleTranslatorFactoryServiceImpl.java @@ -208,8 +208,12 @@ private static Iterable cssStyledTextFromTranslatable(String text String s = attributes != null ? attributes.getDictionaryIdentifier() : null; if (s != null || parentStyle != null) style = cssParser.parse(s != null ? s : "", parentStyle); - if (hyphenating && (style == null || style.getProperty("hyphens") == null)) - style = HYPHENS_AUTO.inheritFrom(style); + if (hyphenating) { + if (style == null) + style = HYPHENS_AUTO; + else if (style.getProperty("hyphens") == null) + style = HYPHENS_AUTO.inheritFrom(style); + } } if (attributes != null && attributes.hasChildren()) return cssStyledTextFromTranslatable(text, attributes.iterator(), language, false, style); @@ -306,28 +310,29 @@ private static Iterable cssStyledTextFromTranslatable(List styledText = new ArrayList<>(); for (Object t : precedingOrFollowingText) { String text; - String locale; + String lang; boolean hyphenate; { if (t instanceof PrecedingText) { text = ((PrecedingText)t).resolve(); - locale = ((PrecedingText)t).getLocale().orElse(null); + lang = ((PrecedingText)t).getLocale().orElse(null); hyphenate = ((PrecedingText)t).shouldHyphenate(); } else if (t instanceof FollowingText) { text = ((FollowingText)t).peek(); - locale = ((FollowingText)t).getLocale().orElse(null); + lang = ((FollowingText)t).getLocale().orElse(null); hyphenate = ((FollowingText)t).shouldHyphenate(); } else throw new RuntimeException(); } - SimpleInlineStyle style; { - if (hyphenate && (parentStyle == null || parentStyle.getProperty("hyphens") == null)) - style = new SimpleInlineStyle("hyphens: auto", parentStyle); - else if (parentStyle == null) - style = null; + Locale locale = lang != null ? parseLocale(lang) : null; + SimpleInlineStyle style = null; { + if (hyphenate && parentStyle == null) + style = HYPHENS_AUTO; + else if (hyphenate && parentStyle.getProperty("hyphens") == null) + style = HYPHENS_AUTO.inheritFrom(parentStyle); else - style = new SimpleInlineStyle("", parentStyle); + style = parentStyle; } - styledText.add(new CSSStyledText(text, style, locale != null ? parseLocale(locale) : null)); + styledText.add(new CSSStyledText(text, style, locale)); } return styledText; } diff --git a/modules/braille/dotify-utils/src/main/resources/META-INF/catalog.xml b/modules/braille/dotify-utils/src/main/resources/META-INF/catalog.xml index 085d90a27b..4013a19248 100644 --- a/modules/braille/dotify-utils/src/main/resources/META-INF/catalog.xml +++ b/modules/braille/dotify-utils/src/main/resources/META-INF/catalog.xml @@ -1,4 +1,7 @@ + + + diff --git a/modules/parent/pom.xml b/modules/parent/pom.xml index 37d255011f..7bf216332e 100644 --- a/modules/parent/pom.xml +++ b/modules/parent/pom.xml @@ -436,6 +436,11 @@ pom import + + org.daisy.pipeline + ds-to-spi-runtime + 1.2.1-SNAPSHOT + org.daisy.pipeline ds-to-spi-runtime diff --git a/modules/scripts-utils/css-utils/src/test/java/org/daisy/pipeline/css/CounterStyleTest.java b/modules/scripts-utils/css-utils/src/test/java/org/daisy/pipeline/css/CounterStyleTest.java index 87c13508c2..931c75c65b 100644 --- a/modules/scripts-utils/css-utils/src/test/java/org/daisy/pipeline/css/CounterStyleTest.java +++ b/modules/scripts-utils/css-utils/src/test/java/org/daisy/pipeline/css/CounterStyleTest.java @@ -3,7 +3,7 @@ import java.util.ArrayList; import java.util.List; -import org.daisy.pipeline.css.CounterStyle.AdditiveTuple; +import org.daisy.pipeline.css.CounterStyleImpl.AdditiveTuple; import org.junit.Assert; import org.junit.Test; @@ -21,31 +21,31 @@ public void testCounterRepresentation() { symbols.add("f"); symbols.add("g"); symbols.add("h"); - Assert.assertEquals("", CounterStyle.counterRepresentationAlphabetic(0, symbols).orElse("")); - Assert.assertEquals("a", CounterStyle.counterRepresentationAlphabetic(1, symbols).orElse("")); - Assert.assertEquals("b", CounterStyle.counterRepresentationAlphabetic(2, symbols).orElse("")); - Assert.assertEquals("add", CounterStyle.counterRepresentationAlphabetic(100, symbols).orElse("")); - Assert.assertEquals("", CounterStyle.counterRepresentationAlphabetic(-1, symbols).orElse("")); - Assert.assertEquals("h", CounterStyle.counterRepresentationCyclic(0, symbols)); - Assert.assertEquals("a", CounterStyle.counterRepresentationCyclic(1, symbols)); - Assert.assertEquals("b", CounterStyle.counterRepresentationCyclic(2, symbols)); - Assert.assertEquals("d", CounterStyle.counterRepresentationCyclic(100, symbols)); - Assert.assertEquals("g", CounterStyle.counterRepresentationCyclic(-1, symbols)); - Assert.assertEquals("", CounterStyle.counterRepresentationFixed(0, symbols).orElse("")); - Assert.assertEquals("a", CounterStyle.counterRepresentationFixed(1, symbols).orElse("")); - Assert.assertEquals("b", CounterStyle.counterRepresentationFixed(2, symbols).orElse("")); - Assert.assertEquals("", CounterStyle.counterRepresentationFixed(100, symbols).orElse("")); - Assert.assertEquals("", CounterStyle.counterRepresentationFixed(-1, symbols).orElse("")); - Assert.assertEquals("a", CounterStyle.counterRepresentationNumeric(0, symbols, "-")); - Assert.assertEquals("b", CounterStyle.counterRepresentationNumeric(1, symbols, "-")); - Assert.assertEquals("c", CounterStyle.counterRepresentationNumeric(2, symbols, "-")); - Assert.assertEquals("bee", CounterStyle.counterRepresentationNumeric(100, symbols, "-")); - Assert.assertEquals("-b", CounterStyle.counterRepresentationNumeric(-1, symbols, "-")); - Assert.assertEquals("", CounterStyle.counterRepresentationSymbolic(0, symbols).orElse("")); - Assert.assertEquals("a", CounterStyle.counterRepresentationSymbolic(1, symbols).orElse("")); - Assert.assertEquals("b", CounterStyle.counterRepresentationSymbolic(2, symbols).orElse("")); - Assert.assertEquals("ddddddddddddd", CounterStyle.counterRepresentationSymbolic(100, symbols).orElse("")); - Assert.assertEquals("", CounterStyle.counterRepresentationSymbolic(-1, symbols).orElse("")); + Assert.assertEquals("", CounterStyleImpl.counterRepresentationAlphabetic(0, symbols).orElse("")); + Assert.assertEquals("a", CounterStyleImpl.counterRepresentationAlphabetic(1, symbols).orElse("")); + Assert.assertEquals("b", CounterStyleImpl.counterRepresentationAlphabetic(2, symbols).orElse("")); + Assert.assertEquals("add", CounterStyleImpl.counterRepresentationAlphabetic(100, symbols).orElse("")); + Assert.assertEquals("", CounterStyleImpl.counterRepresentationAlphabetic(-1, symbols).orElse("")); + Assert.assertEquals("h", CounterStyleImpl.counterRepresentationCyclic(0, symbols)); + Assert.assertEquals("a", CounterStyleImpl.counterRepresentationCyclic(1, symbols)); + Assert.assertEquals("b", CounterStyleImpl.counterRepresentationCyclic(2, symbols)); + Assert.assertEquals("d", CounterStyleImpl.counterRepresentationCyclic(100, symbols)); + Assert.assertEquals("g", CounterStyleImpl.counterRepresentationCyclic(-1, symbols)); + Assert.assertEquals("", CounterStyleImpl.counterRepresentationFixed(0, symbols).orElse("")); + Assert.assertEquals("a", CounterStyleImpl.counterRepresentationFixed(1, symbols).orElse("")); + Assert.assertEquals("b", CounterStyleImpl.counterRepresentationFixed(2, symbols).orElse("")); + Assert.assertEquals("", CounterStyleImpl.counterRepresentationFixed(100, symbols).orElse("")); + Assert.assertEquals("", CounterStyleImpl.counterRepresentationFixed(-1, symbols).orElse("")); + Assert.assertEquals("a", CounterStyleImpl.counterRepresentationNumeric(0, symbols, "-")); + Assert.assertEquals("b", CounterStyleImpl.counterRepresentationNumeric(1, symbols, "-")); + Assert.assertEquals("c", CounterStyleImpl.counterRepresentationNumeric(2, symbols, "-")); + Assert.assertEquals("bee", CounterStyleImpl.counterRepresentationNumeric(100, symbols, "-")); + Assert.assertEquals("-b", CounterStyleImpl.counterRepresentationNumeric(-1, symbols, "-")); + Assert.assertEquals("", CounterStyleImpl.counterRepresentationSymbolic(0, symbols).orElse("")); + Assert.assertEquals("a", CounterStyleImpl.counterRepresentationSymbolic(1, symbols).orElse("")); + Assert.assertEquals("b", CounterStyleImpl.counterRepresentationSymbolic(2, symbols).orElse("")); + Assert.assertEquals("ddddddddddddd", CounterStyleImpl.counterRepresentationSymbolic(100, symbols).orElse("")); + Assert.assertEquals("", CounterStyleImpl.counterRepresentationSymbolic(-1, symbols).orElse("")); } @Test @@ -64,7 +64,7 @@ public void testCounterRepresentationAdditive() { symbols.add(new AdditiveTuple(5, "⠧")); symbols.add(new AdditiveTuple(4, "⠊⠧")); symbols.add(new AdditiveTuple(1, "⠊")); - Assert.assertEquals("⠍⠉⠍⠭⠉⠧⠊⠊⠊", CounterStyle.counterRepresentationAdditive(1998, symbols).orElse("")); + Assert.assertEquals("⠍⠉⠍⠭⠉⠧⠊⠊⠊", CounterStyleImpl.counterRepresentationAdditive(1998, symbols).orElse("")); } @Test diff --git a/modules/tts/tts-common/src/main/java/org/daisy/pipeline/tts/DefaultSpeechRate.java b/modules/tts/tts-common/src/main/java/org/daisy/pipeline/tts/DefaultSpeechRate.java index c39f553096..8f272f4804 100644 --- a/modules/tts/tts-common/src/main/java/org/daisy/pipeline/tts/DefaultSpeechRate.java +++ b/modules/tts/tts-common/src/main/java/org/daisy/pipeline/tts/DefaultSpeechRate.java @@ -1,7 +1,11 @@ package org.daisy.pipeline.tts; +import java.util.HashMap; import java.util.Map; +import cz.vutbr.web.css.CSSProperty; +import cz.vutbr.web.css.CSSProperty.SpeechRate; +import cz.vutbr.web.css.Declaration; import cz.vutbr.web.css.SupportedCSS; import cz.vutbr.web.css.Term; import cz.vutbr.web.css.TermIdent; @@ -10,8 +14,7 @@ import cz.vutbr.web.domassign.DeclarationTransformer; import cz.vutbr.web.domassign.SupportedCSS21; -import org.daisy.braille.css.BrailleCSSParserFactory; -import org.daisy.braille.css.SimpleInlineStyle; // SimpleInlineStyle lives in braille-css but can be used for regular CSS too +import org.daisy.braille.css.BrailleCSSParserFactory; // BrailleCSSParserFactory can be used for regular CSS too import org.daisy.common.properties.Properties; import org.daisy.common.properties.Properties.Property; import org.daisy.pipeline.css.speech.SpeechDeclarationTransformer; @@ -35,32 +38,43 @@ public DefaultSpeechRate() {} * with 1.0 corresponding with the normal speaking rate of 200 words per minute. */ public float getValue(Map properties) { - SimpleInlineStyle s = new SimpleInlineStyle( - parserFactory.parseSimpleInlineStyle("speech-rate: " + SPEECH_RATE.getValue(properties)), - null, - declarationTransformer, - speechCSS); - Term t = s.getValue("speech-rate"); - if (t != null) { - if (t instanceof TermPercent) { - return ((TermPercent)t).getValue() / 100; - } else if (t instanceof TermInteger) { // words per minute - return ((TermInteger)t).getValue() / 200; - } else if (t instanceof TermIdent) { - String i = ((TermIdent)t).getValue(); - if ("x-slow".equalsIgnoreCase(i)) { - return 0.4f; // 80 words per minute - } else if ("slow".equalsIgnoreCase(i)) { - return 0.6f; // 120 words per minute - } else if ("medium".equalsIgnoreCase(i)) { - return 1.0f; // 200 words per minute - } else if ("fast".equalsIgnoreCase(i)) { - return 1.5f; // 300 words per minute - } else if ("x-fast".equalsIgnoreCase(i)) { - return 2.5f; // 500 words per minute - } + Iterable declarations + = parserFactory.parseSimpleInlineStyle("speech-rate: " + SPEECH_RATE.getValue(properties)); + if (declarations != null) + for (Declaration d : declarations) { + Map props = new HashMap<>(); + Map> terms = new HashMap<>(); + if (declarationTransformer.parseDeclaration(d, props, terms)) + for (String name: props.keySet()) { + CSSProperty p = props.get(name); + if (p instanceof SpeechRate) + switch ((SpeechRate)p) { + case X_SLOW: + return 0.4f; // 80 words per minute + case SLOW: + return 0.6f; // 120 words per minute + case MEDIUM: + return 1.0f; // 200 words per minute + case FAST: + return 1.5f; // 300 words per minute + case X_FAST: + return 2.5f; // 500 words per minute + case number: + Term t = terms.get(name); + if (t != null) + if (t instanceof TermPercent) + return ((TermPercent)t).getValue() / 100; + else if (t instanceof TermInteger) // words per minute + return ((TermInteger)t).getValue() / 200; + break; + case FASTER: + case SLOWER: + case INHERIT: + case INITIAL: + default: + } + } } - } return 1.0f; } } From 9a1abaa0459c4f47447b1cd0e2efa4e53613296d Mon Sep 17 00:00:00 2001 From: Bert Frees Date: Tue, 3 Dec 2024 15:57:12 +0100 Subject: [PATCH 14/15] git subrepo pull cli subrepo: subdir: "cli" merged: "9e483116d0" upstream: origin: "git@github.com:daisy/pipeline-cli-go.git" branch: "master" commit: "9e483116d0" git-subrepo: version: "0.3.1" origin: "???" commit: "???" --- cli/.gitrepo | 4 ++-- cli/pom.xml | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/cli/.gitrepo b/cli/.gitrepo index 4a32282e79..d920e3f90b 100644 --- a/cli/.gitrepo +++ b/cli/.gitrepo @@ -6,6 +6,6 @@ [subrepo] remote = git@github.com:daisy/pipeline-cli-go.git branch = master - commit = 080925324c4dbd37d141261f537bb2a5e6fadde3 - parent = 0d87113b214274de885978a9efe04f6c878fa94c + commit = 9e483116d0ffc78d5bdeb73ca00918c1b0c00e14 + parent = 7f81547a070c3fbfbc4bc2ba15c2a7196c396913 cmdver = 0.3.1 diff --git a/cli/pom.xml b/cli/pom.xml index 2c8c8701aa..ae37eeb013 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -12,7 +12,7 @@ org.daisy.pipeline cli - 2.2.0 + 2.2.1-SNAPSHOT pom DAISY Pipeline 2 :: Command Line Interface Command Line Interface for the DAISY Pipeline 2. @@ -101,8 +101,4 @@ - - - v2.2.0 - From 331d295487836bcb511de6029b2cb4449a04ca75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:12:58 +0000 Subject: [PATCH 15/15] Bump http-proxy-middleware from 2.0.6 to 2.0.7 in /ui Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.6 to 2.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.7/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.6...v2.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-type: indirect ... Signed-off-by: dependabot[bot] --- ui/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/yarn.lock b/ui/yarn.lock index 516d2fe6c0..7f51d99387 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -5519,9 +5519,9 @@ http-proxy-agent@^5.0.0: debug "4" http-proxy-middleware@^2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" - integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + version "2.0.7" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz#915f236d92ae98ef48278a95dedf17e991936ec6" + integrity sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA== dependencies: "@types/http-proxy" "^1.17.8" http-proxy "^1.18.1"