Merge remote-tracking branch 'origin/GP-4970-dragonmacher-help-fix'

This commit is contained in:
Ryan Kurtz 2024-10-04 07:38:12 -04:00
commit 6315aa68df
20 changed files with 261 additions and 159 deletions

View file

@ -830,6 +830,7 @@ public class Application {
/**
* Returns a collection of module library directories. Library directories are optional for a module.
* @return a collection of module library directories.
* @see ModuleUtilities#getModuleLibDirectories(Collection)
*/
public static Collection<ResourceFile> getLibraryDirectories() {
checkAppInitialized();

View file

@ -51,12 +51,14 @@ public class GHelpBuilder {
private static final String OUTPUT_DIRECTORY_OPTION = "-o";
private static final String MODULE_NAME_OPTION = "-n";
private static final String HELP_PATHS_OPTION = "-hp";
private static final String HELP_PATHS_GENERATED_OPTION = "-hpg";
private static final String DEBUG_SWITCH = "-debug";
private static final String IGNORE_INVALID_SWITCH = "-ignoreinvalid";
private String outputDirectoryName;
private String moduleName;
private Collection<File> dependencyHelpPaths = new LinkedHashSet<>();
private Collection<File> generatedDependencyHelpPaths = new LinkedHashSet<>();
private Collection<File> helpInputDirectories = new LinkedHashSet<>();
private static boolean debugEnabled = false;
private boolean ignoreInvalid = false; // TODO: Do actual validation here
@ -112,7 +114,22 @@ public class GHelpBuilder {
for (File file : dependencyHelpPaths) {
allHelp.add(file);
}
return HelpModuleCollection.fromFiles(allHelp);
HelpModuleCollection help = HelpModuleCollection.fromFiles(allHelp);
for (File file : generatedDependencyHelpPaths) {
help.addGeneratedHelpLocation(file);
}
return help;
}
private HelpModuleCollection collectInputHelp() {
HelpModuleCollection help = HelpModuleCollection.fromFiles(helpInputDirectories);
for (File file : generatedDependencyHelpPaths) {
help.addGeneratedHelpLocation(file);
}
return help;
}
private Results validateHelpDirectories(HelpModuleCollection help, LinkDatabase linkDatabase) {
@ -163,11 +180,11 @@ public class GHelpBuilder {
JavaHelpFilesBuilder fileBuilder =
new JavaHelpFilesBuilder(outputDirectory, moduleName, linkDatabase);
HelpModuleCollection help = HelpModuleCollection.fromFiles(helpInputDirectories);
HelpModuleCollection helpInput = collectInputHelp();
// 1) Generate JavaHelp files for the module (e.g., TOC file, map file)
try {
fileBuilder.generateHelpFiles(help);
fileBuilder.generateHelpFiles(helpInput);
}
catch (Exception e) {
exitWithError("Unexpected error building help module files:\n", e);
@ -302,6 +319,20 @@ public class GHelpBuilder {
}
}
}
else if (opt.equals(HELP_PATHS_GENERATED_OPTION)) {
i++;
if (i >= args.length) {
errorMessage(HELP_PATHS_GENERATED_OPTION + " requires an argument");
printUsage();
System.exit(1);
}
String hp = args[i];
if (hp.length() > 0) {
for (String p : hp.split(File.pathSeparator)) {
generatedDependencyHelpPaths.add(new File(p));
}
}
}
else if (opt.equals(DEBUG_SWITCH)) {
debugEnabled = true;
}

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -21,7 +21,8 @@ import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -36,7 +37,6 @@ import javax.swing.text.html.HTML.Tag;
import generic.jar.ResourceFile;
import generic.theme.GColor;
import generic.theme.Gui;
import ghidra.framework.Application;
import ghidra.framework.preferences.Preferences;
import ghidra.util.Msg;
import resources.*;
@ -183,6 +183,10 @@ public class GHelpHTMLEditorKit extends HTMLEditorKit {
*/
private HyperlinkEvent validateURL(HyperlinkEvent event) {
URL url = event.getURL();
if (url == null) {
Msg.trace(this, "No URL for link: " + event);
return maybeCreateNewHyperlinkEventWithUpdatedURL(event);
}
try {
url.openStream();// assume that this will fail if the file does not exist
}
@ -239,7 +243,7 @@ public class GHelpHTMLEditorKit extends HTMLEditorKit {
// directory). See if it may be a relative link to a build's installation root (like
// a file in <install dir>/docs).
//
newUrl = findApplicationfile(HREF);
newUrl = HelpBuildUtils.findApplicationUrl(HREF);
return newUrl;
}
@ -385,37 +389,15 @@ public class GHelpHTMLEditorKit extends HTMLEditorKit {
return url;
}
return findModuleFile("help/shared/" + name);
}
private URL findApplicationfile(String relativePath) {
ResourceFile installDir = Application.getInstallationDirectory();
ResourceFile file = new ResourceFile(installDir, relativePath);
if (file.exists()) {
ResourceFile file = HelpBuildUtils.findModuleFile("help/shared/" + name);
if (file != null) {
try {
return file.toURL();
}
catch (MalformedURLException e) {
Msg.showError(this, null, "Unexpected Error",
"Unexpected error parsing file to URL: " + file);
}
}
return null;
}
private URL findModuleFile(String relativePath) {
Collection<ResourceFile> moduleDirs = Application.getModuleRootDirectories();
for (ResourceFile dir : moduleDirs) {
ResourceFile file = new ResourceFile(dir, relativePath);
if (file.exists()) {
try {
return file.toURL();
}
catch (MalformedURLException e) {
Msg.showError(this, null, "Unexpected Error",
"Unexpected error parsing file to URL: " + file);
return null;
}
return null;
}
}
return null;

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -28,9 +28,8 @@ import javax.help.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import generic.jar.ResourceFile;
import ghidra.framework.Application;
import ghidra.util.SystemUtilities;
import help.validator.JavaHelpValidator;
/**
* Ghidra help set that creates a GhidraHelpBroker, installs some custom HTML handling code via
@ -201,17 +200,22 @@ public class GHelpSet extends HelpSet {
/**
* This is meant for help files that are not included in the standard help system. Their
* id paths are expected to be relative to the application install directory.
* @param id the help id.
* @param rawId the help id.
* @return the URL to the help file.
*/
private URL tryToCreateURLFromID(String id) {
private URL tryToCreateURLFromID(String rawId) {
URL fileURL = createFileURL(id);
if (fileURL != null) {
return fileURL;
String idText = rawId;
if (rawId.startsWith(JavaHelpValidator.EXTERNAL_PREFIX)) {
idText = rawId.substring(JavaHelpValidator.EXTERNAL_PREFIX.length());
}
URL rawURL = createRawURL(id);
URL fileUrl = HelpBuildUtils.findApplicationUrl(idText);
if (fileUrl != null) {
return fileUrl;
}
URL rawURL = createRawURL(idText);
return rawURL;
}
@ -238,31 +242,6 @@ public class GHelpSet extends HelpSet {
return null;
}
private URL createFileURL(String id) {
ResourceFile helpFile = fileFromID(id);
if (!helpFile.exists()) {
LOG.trace("ID is not a file; tried: " + helpFile);
return null;
}
try {
return helpFile.toURL();
}
catch (MalformedURLException e) {
// this shouldn't happen, as the file exists
LOG.trace("ID is not a URL; tried to make URL from file: " + helpFile);
}
return null;
}
private ResourceFile fileFromID(String id) {
// this allows us to find files by using relative paths (e.g., 'docs/WhatsNew.html'
// will get resolved relative to the installation directory in a build).
ResourceFile installDir = Application.getInstallationDirectory();
ResourceFile helpFile = new ResourceFile(installDir, id);
return helpFile;
}
@Override
public boolean isID(URL url) {
return mapDelegate.isID(url);

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -30,7 +30,8 @@ import generic.theme.GIcon;
import generic.theme.GThemeDefaults.Colors;
import generic.theme.Gui;
import ghidra.framework.Application;
import ghidra.util.HelpLocation;
import ghidra.util.*;
import help.validator.JavaHelpValidator;
import help.validator.location.*;
import resources.IconProvider;
import resources.Icons;
@ -166,6 +167,71 @@ public class HelpBuildUtils {
return null;
}
/**
* Finds the actual module file for the given relative path when in development mode. For
* example, given:
* <pre>
* help/shared/DefaultStyle.css
* </pre>
* This method will find:
* <pre>
* {repo}/Ghidra/Framework/Help/src/main/resources/help/shared/DefaultStyle.css
* </pre>
* @param relativePath the path
* @return the file
*/
public static ResourceFile findModuleFile(String relativePath) {
Collection<ResourceFile> moduleDirs = Application.getModuleRootDirectories();
for (ResourceFile dir : moduleDirs) {
ResourceFile file = new ResourceFile(dir, relativePath);
if (file.exists()) {
return file;
}
}
return null;
}
/**
* Searches the application classpath (for a module file) and directory structure (for a release
* file), depending on whether in release mode or development mode, to find the URL for the
* given relative path.
* @param relativePath the path
* @return the URL or null if not found
*/
public static URL findApplicationUrl(String relativePath) {
String updatedPath = relativePath;
if (relativePath.startsWith(JavaHelpValidator.EXTERNAL_PREFIX)) {
updatedPath = relativePath.substring(JavaHelpValidator.EXTERNAL_PREFIX.length());
}
ResourceFile file = null;
if (SystemUtilities.isInDevelopmentMode()) {
// example: "docs/WhatsNew.html", which lives in a source dir in dev mode
file = findModuleFile("src/global/" + updatedPath);
}
else {
//
// In release mode, some of the help content is relative to the root of the application,
// such as 'docs/WhatsNew.html'.
//
ResourceFile installDir = Application.getInstallationDirectory();
file = new ResourceFile(installDir, updatedPath);
}
if (file == null || !file.exists()) {
return null;
}
try {
return file.toURL();
}
catch (MalformedURLException e) {
Msg.trace(HelpBuildUtils.class, "Unexpected error parsing file to URL: " + file);
}
return null;
}
//==================================================================================================
// Cleanup Methods
//==================================================================================================

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -220,6 +220,7 @@ public class JavaHelpFilesBuilder {
Files.delete(file);
}
catch (IOException e) {
// ignore
}
}
}

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -29,23 +29,13 @@ import help.validator.location.HelpModuleCollection;
import help.validator.model.*;
public class JavaHelpValidator {
// This allows help links to signal that the reference is pointing to a file that lives outside
// of the help system
public static final String EXTERNAL_PREFIX = "external:";
private static boolean debug;
/** Files that are generated and may not exist at validation time */
private static Set<String> EXCLUDED_FILE_NAMES = createExcludedFileSet();
private static Set<String> createExcludedFileSet() {
Set<String> set = new HashSet<>();
// The expected format is the help path, without an extension (this helps catch multiple
// references with anchors)
set.add("help/topics/Misc/Tips");
set.add("docs/WhatsNew");
set.add("docs/README_PDB");
return set;
}
private String moduleName;
private HelpModuleCollection help;
@ -304,7 +294,7 @@ public class JavaHelpValidator {
path = path.substring(0, index);
}
return EXCLUDED_FILE_NAMES.contains(path);
return path.startsWith(EXTERNAL_PREFIX);
}
private void validateExternalFileLinks(LinkDatabase linkDatabase) {

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -15,6 +15,12 @@
*/
package help.validator;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.Map.Entry;
import help.OverlayHelpTree;
import help.TOCItemProvider;
import help.validator.links.InvalidHREFLink;
@ -22,12 +28,6 @@ import help.validator.links.InvalidLink;
import help.validator.location.HelpModuleCollection;
import help.validator.model.*;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.Map.Entry;
public class LinkDatabase {
/** Sorted for later presentation */

View file

@ -0,0 +1,43 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package help.validator.location;
import java.io.File;
import help.validator.model.GhidraTOCFile;
/**
* Represents a directory that holds generated content. At the time of writing, the only known
* such input is the 'tips of the day' html file that is created from a text file.
*/
public class GeneratedDirectoryHelpModuleLocation extends DirectoryHelpModuleLocation {
public GeneratedDirectoryHelpModuleLocation(File file) {
super(file);
}
@Override
public GhidraTOCFile loadSourceTOCFile() {
// Generated directories are not full help directories with TOC source files.
return null;
}
@Override
public boolean isHelpInputSource() {
return false;
}
}

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -16,21 +16,10 @@
package help.validator.location;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.*;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
import javax.help.HelpSet;
@ -38,19 +27,9 @@ import javax.help.Map.ID;
import javax.help.TOCView;
import javax.swing.tree.DefaultMutableTreeNode;
import help.*;
import help.CustomTOCView.CustomTreeItemDecorator;
import help.HelpBuildUtils;
import help.PathKey;
import help.TOCItemProvider;
import help.validator.model.AnchorDefinition;
import help.validator.model.GhidraTOCFile;
import help.validator.model.HREF;
import help.validator.model.HelpFile;
import help.validator.model.HelpTopic;
import help.validator.model.IMG;
import help.validator.model.TOCItem;
import help.validator.model.TOCItemDefinition;
import help.validator.model.TOCItemExternal;
import help.validator.model.*;
/**
* A class that is meant to hold a single help <b>input</b> directory and 0 or more
@ -130,12 +109,21 @@ public class HelpModuleCollection implements TOCItemProvider {
if (inputHelp == null && externalHelpSets.size() == 0) {
throw new IllegalArgumentException(
"Required TOC file does not exist. " + "You must create a TOC_Source.xml file, " +
"Required TOC file does not exist. You must create a TOC_Source.xml file, " +
"even if it is an empty template, or provide a pre-built TOC. " +
"Help directories: " + locations.toString());
}
}
public void addGeneratedHelpLocation(File file) {
HelpModuleLocation location = new GeneratedDirectoryHelpModuleLocation(file);
helpLocations.add(location);
HelpSet helpSet = location.getHelpSet();
if (helpSet != null) {
externalHelpSets.add(helpSet);
}
}
public GhidraTOCFile getSourceTOCFile() {
return inputHelp.getSourceTOCFile();
}
@ -160,17 +148,17 @@ public class HelpModuleCollection implements TOCItemProvider {
externalHelpSets = new ArrayList<>();
for (HelpModuleLocation location : helpLocations) {
if (location.isHelpInputSource()) {
continue; // help sets only exist in pre-built help
}
doAddHelpSet(location);
}
}
HelpSet helpSet = location.getHelpSet();
externalHelpSets.add(helpSet);
private void doAddHelpSet(HelpModuleLocation location) {
if (location.isHelpInputSource()) {
return; // help sets only exist in pre-built help
}
if (externalHelpSets.isEmpty()) {
return;
}
HelpSet helpSet = location.getHelpSet();
externalHelpSets.add(helpSet);
}
public boolean containsHelpFiles() {
@ -230,7 +218,8 @@ public class HelpModuleCollection implements TOCItemProvider {
public Collection<AnchorDefinition> getAllAnchorDefinitions() {
List<AnchorDefinition> result = new ArrayList<>();
for (HelpModuleLocation location : helpLocations) {
result.addAll(location.getAllAnchorDefinitions());
Collection<AnchorDefinition> anchors = location.getAllAnchorDefinitions();
result.addAll(anchors);
}
return result;
}
@ -250,7 +239,6 @@ public class HelpModuleCollection implements TOCItemProvider {
if (helpPath == null) {
return null;
}
Map<PathKey, HelpFile> map = getPathHelpFileMap();
return map.get(new PathKey(helpPath));
}
@ -373,4 +361,5 @@ public class HelpModuleCollection implements TOCItemProvider {
public String toString() {
return helpLocations.toString();
}
}

View file

@ -55,6 +55,7 @@ public abstract class HelpModuleLocation {
public abstract boolean isHelpInputSource();
protected void loadHelpTopics() {
Path helpTopicsDir = helpDir.resolve("topics");
if (!Files.exists(helpTopicsDir)) {
HelpBuildUtils.debug("No topics found in help dir: " + this);