docs: public boolean preValidateTopic(final SpecTopic specTopic, final Map<String, SpecTopic> specTopics,
              final Set<String> processedFixedUrls, final BookType bookType, boolean allowRelationships, final ContentSpec contentSpec) {
          // Check if the app should be shutdown
          if (isShuttingDown.get()) {
              shutdown.set(true);
              return false;
          }
  
          boolean valid = true;
  
          // Checks that the id isn't null and is a valid topic ID
          if (specTopic.getId() == null || !specTopic.getId().matches(CSConstants.ALL_TOPIC_ID_REGEX)) {
              log.error(String.format(ProcessorConstants.ERROR_INVALID_TOPIC_ID_MSG, specTopic.getLineNumber(), specTopic.getText()));
              valid = false;
          }
  
          if ((bookType == BookType.BOOK || bookType == BookType.BOOK_DRAFT) && specTopic.getParent() instanceof Level) {
              final Level parent = (Level) specTopic.getParent();
              // Check that the topic is inside a chapter/section/process/appendix/part/preface
              final LevelType parentLevelType = parent.getLevelType();
              if (parent == null || parentLevelType == LevelType.BASE) {
                  log.error(format(ProcessorConstants.ERROR_TOPIC_OUTSIDE_CHAPTER_MSG, specTopic.getLineNumber(), specTopic.getText()));
                  valid = false;
              }
  
              // Check that there are no levels in the parent part (ie the topic is in the intro)
              if (parent != null && parentLevelType == LevelType.PART) {
                  final List<Node> parentChildren = parent.getChildNodes();
                  final int index = parentChildren.indexOf(specTopic);
  
                  for (int i = 0; i < index; i++) {
                      final Node node = parentChildren.get(i);
                      if (node instanceof Level && ((Level) node).getLevelType() != LevelType.INITIAL_CONTENT) {
                          log.error(String.format(ProcessorConstants.ERROR_TOPIC_NOT_IN_PART_INTRO_MSG, specTopic.getLineNumber(),
                                  specTopic.getText()));
                          valid = false;
                          break;
                      }
                  }
              }
          }
  
          // Check that the title exists
          if (isNullOrEmpty(specTopic.getTitle())) {
              log.error(String.format(ProcessorConstants.ERROR_TOPIC_NO_TITLE_MSG, specTopic.getLineNumber(), specTopic.getText()));
              valid = false;
          }
  
          // Check that we aren't using translations for anything but existing topics
          if (!specTopic.isTopicAnExistingTopic()) {
              // Check that we aren't processing translations
              if (processingOptions.isTranslation()) {
                  log.error(String.format(ProcessorConstants.ERROR_TOPIC_NO_NEW_TRANSLATION_TOPIC, specTopic.getLineNumber(),
                          specTopic.getText()));
                  valid = false;
              }
          }
  
          // Check that the initial content topic doesn't have a fixed url and if it does then print a warning,
          // otherwise check that the fixed url is valid
          if (specTopic.getTopicType() == TopicType.INITIAL_CONTENT && !isNullOrEmpty(specTopic.getFixedUrl())) {
              log.warn(format(ProcessorConstants.WARN_FIXED_URL_WILL_BE_IGNORED_MSG, specTopic.getLineNumber(), specTopic.getText()));
              specTopic.setFixedUrl(null);
          } else if (!isNullOrEmpty(specTopic.getFixedUrl())) {
              if (!validateFixedUrl(specTopic, processedFixedUrls)) {
                  valid = false;
              } else {
                  processedFixedUrls.add(specTopic.getFixedUrl());
              }
          }
  
          // Check that we are allowed to create new topics
          if (!specTopic.isTopicAnExistingTopic() && !processingOptions.isAllowNewTopics()) {
              log.error(String.format(ProcessorConstants.ERROR_TOPIC_NO_NEW_TOPIC_BUILD, specTopic.getLineNumber(), specTopic.getText()));
              valid = false;
          }
  
          // New Topics
          if (specTopic.isTopicANewTopic()) {
              if (isNullOrEmpty(specTopic.getType())) {
                  log.error(String.format(ProcessorConstants.ERROR_TOPIC_NO_TYPE_MSG, specTopic.getLineNumber(), specTopic.getText()));
                  valid = false;
              }
              // Existing Topics
          } else if (specTopic.isTopicAnExistingTopic()) {
              // Check that tags aren't trying to be removed
              if (!specTopic.getRemoveTags(false).isEmpty()) {
                  log.error(String.format(ProcessorConstants.ERROR_TOPIC_EXISTING_TOPIC_CANNOT_REMOVE_TAGS, specTopic.getLineNumber(),
                          specTopic.getText()));
                  valid = false;
              }
  
              // Check that tags aren't trying to be added to a revision
              if (processingOptions.isPrintChangeWarnings() && specTopic.getRevision() != null && !specTopic.getTags(false).isEmpty()) {
                  log.warn(
                          String.format(ProcessorConstants.WARN_TAGS_IGNORE_MSG, specTopic.getLineNumber(), "revision", specTopic.getText()));
              }
  
              // Check that urls aren't trying to be added
              if (!specTopic.getSourceUrls(false).isEmpty()) {
                  log.error(String.format(ProcessorConstants.ERROR_TOPIC_EXISTING_TOPIC_CANNOT_ADD_SOURCE_URLS, specTopic.getLineNumber(),
                          specTopic.getText()));
                  valid = false;
              }
  
              // Check that the assigned writer and description haven't been set
              if (specTopic.getAssignedWriter(false) != null || specTopic.getDescription(false) != null) {
                  log.error(
                          String.format(ProcessorConstants.ERROR_TOPIC_EXISTING_BAD_OPTIONS, specTopic.getLineNumber(), specTopic.getText()));
                  valid = false;
              }
  
              // Check that we aren't processing translations
              if (!specTopic.getTags(true).isEmpty() && processingOptions.isTranslation()) {
                  log.error(String.format(ProcessorConstants.ERROR_TOPIC_NO_TAGS_TRANSLATION_TOPIC, specTopic.getLineNumber(),
                          specTopic.getText()));
                  valid = false;
              }
          }
          // Duplicated Topics
          else if (specTopic.isTopicADuplicateTopic()) {
              String temp = "N" + specTopic.getId().substring(1);
  
              // Check that the topic exists in the content specification
              if (!specTopics.containsKey(temp)) {
                  log.error(String.format(ProcessorConstants.ERROR_TOPIC_NONEXIST_MSG, specTopic.getLineNumber(), specTopic.getText()));
                  valid = false;
              } else {
                  // Check that the topic titles match the original
                  if (!specTopic.getTitle().equals(specTopics.get(temp).getTitle())) {
                      String topicTitleMsg = "Topic " + specTopic.getId() + ": " + specTopics.get(temp).getTitle();
                      log.error(String.format(ProcessorConstants.ERROR_TOPIC_TITLES_NONMATCH_MSG, specTopic.getLineNumber(),
                              specTopic.getText(), topicTitleMsg));
                      valid = false;
                  }
              }
              // Cloned Topics
          } else if (specTopic.isTopicAClonedTopic()) {
              // Check if a description or type exists. If one does then generate an error.
              if ((specTopic.getType() != null && !specTopic.getType().equals("")) || (specTopic.getDescription(
                      false) != null && !specTopic.getDescription(false).equals(""))) {
                  log.error(String.format(ProcessorConstants.ERROR_TOPIC_CLONED_BAD_OPTIONS, specTopic.getLineNumber(), specTopic.getText()));
                  valid = false;
              }
              // Duplicated Cloned Topics
          } else if (specTopic.isTopicAClonedDuplicateTopic()) {
              // Find the duplicate topic in the content spec
              final String topicId = specTopic.getId().substring(1);
              int count = 0;
              SpecTopic clonedTopic = null;
              for (final Entry<String, SpecTopic> entry : specTopics.entrySet()) {
                  final String uniqueTopicId = entry.getKey();
  
                  if (uniqueTopicId.endsWith(topicId) && !uniqueTopicId.endsWith(specTopic.getId())) {
                      clonedTopic = entry.getValue();
                      count++;
                  }
              }
  
              // Check that the topic exists
              if (count == 0) {
                  log.error(String.format(ProcessorConstants.ERROR_TOPIC_NONEXIST_MSG, specTopic.getLineNumber(), specTopic.getText()));
                  valid = false;
              }
              // Check that the referenced topic is unique
              else if (count > 1) {
                  log.error(
                          String.format(ProcessorConstants.ERROR_TOPIC_DUPLICATE_CLONES_MSG, specTopic.getLineNumber(), specTopic.getText()));
                  valid = false;
              } else {
                  // Check that the title matches
                  if (!specTopic.getTitle().equals(clonedTopic.getTitle())) {
                      String topicTitleMsg = "Topic " + specTopic.getId() + ": " + clonedTopic.getTitle();
                      log.error(String.format(ProcessorConstants.ERROR_TOPIC_TITLES_NONMATCH_MSG, specTopic.getLineNumber(),
                              specTopic.getText(), topicTitleMsg));
                      valid = false;
                  }
              }
          }
  
          // Check to make sure no relationships exist if they aren't allowed
          if (!allowRelationships && specTopic.getRelationships().size() > 0) {
              log.error(format(ProcessorConstants.ERROR_TOPIC_HAS_RELATIONSHIPS_MSG, specTopic.getLineNumber(), specTopic.getText()));
              valid = false;
          }
  
          // Check for conflicting conditions
          checkForConflictingCondition(specTopic, contentSpec);
  
          return valid;
      }