374 DiagnosticReporter reporter(result.diagnostics);
375 const bool cliPipelines = !input.cliPipelines.isEmpty();
377 const auto flagNewOnlySetting = [&](
const QString &var) {
379 "New publication setting '%1' has no effect without an explicit "
380 "'pipeline' selection."_L1.arg(var));
386 for (
const QString &key : config.variableNames()) {
388 flagNewOnlySetting(key);
396 for (
const auto &capability : input.pipelineCapabilities) {
397 if (capability.pipeline == Pipeline::Legacy)
398 legacy = &capability;
400 templated = &capability;
405 const auto selected = filterIdentityTokens(cliPipelines ? input.cliPipelines
406 : pipelineVar.asStringList());
407 if (selected.isEmpty())
409 u"Select at least one pipeline in the configuration or on the command line."_s);
410 for (
const QString &name : selected) {
411 const auto pipeline = pipelineFromName(name);
413 reporter.add(Diagnostic::Kind::Error, pipelineSource,
414 "Pipeline '%1' is not recognized."_L1.arg(name),
415 u"QDoc supports the 'legacy' and 'template' pipelines."_s);
418 const auto *capability = *pipeline == Pipeline::Legacy ? legacy : templated;
419 if (!capability || !capability->available) {
420 reporter.add(Diagnostic::Kind::Error, pipelineSource,
421 "Pipeline '%1' is unavailable."_L1.arg(name));
424 result.pipelines.push_back(*pipeline);
428 const auto configured = filterIdentityTokens(formatsVar.asStringList());
429 const auto requested = input.cliFormats.isEmpty() ? configured
430 : filterIdentityTokens(input.cliFormats);
431 if (requested.isEmpty())
433 input.cliFormats.isEmpty() ? formatsVar.location() :
Location(),
434 u"Format selection is empty."_s,
435 u"Select at least one output format in the configuration or on the command line."_s);
437 const QSet<QString> requestedSet(requested.cbegin(), requested.cend());
441 return capability && (capability->supportedFormats.contains(format)
442 || capability->brokenFormats.contains(format));
444 const auto addFormat = [&](
const QString &format,
const Location &location,
bool configured) {
446 entry.format = format;
447 entry
.requested = requestedSet.contains(format);
449 entry.location = location;
450 if (!isValidFormatIdentity(format)) {
452 "Invalid format identity '%1'."_L1.arg(format),
453 u"Format names may contain letters, digits, and underscores only."_s);
457 if (templateEnabled && declares(templated, format))
459 else if (legacyEnabled && declares(legacy, format))
464 entry.fileExtension = owner->formatExtensions.value(format);
465 if (entry
.requested && owner->brokenFormats.contains(format)) {
467 "Format '%1' is declared by pipeline '%2' but the declaration "
468 "is broken."_L1.arg(format, pipelineName(owner->pipeline)),
469 u"QDoc does not fall back to another pipeline for a broken declaration."_s);
471 const QString extensionKey = configKey(format, u"extension"_s);
472 const auto &extension = config.get(extensionKey);
473 const bool overridden = config.contains(extensionKey);
475 const auto values = extension.asStringList();
476 entry.fileExtension = values.value(0);
479 "%1 must contain a single value."_L1.arg(extensionKey));
481 if (entry
.requested && entry.fileExtension.isEmpty())
483 overridden ? extension.location() : location,
484 "Format '%1' has no file extension."_L1.arg(format),
485 "Provide a pipeline default or a non-empty %1."_L1.arg(extensionKey));
489 for (Pipeline pipeline : result.pipelines)
490 names.append(pipelineName(pipeline));
492 "Format '%1' is not supported by any selected pipeline."_L1.arg(format),
493 names.isEmpty() ? u"No pipeline is available."_s
494 :
"Selected pipelines: %1."_L1.arg(names.join(u", "_s)));
496 result.formats.push_back(
std::move(entry));
499 for (
const QString &format : configured)
500 addFormat(format, formatsVar.location(),
true);
501 for (
const QString &format : requested) {
502 if (!configured.contains(format))
503 addFormat(format, Location(),
false);
684 result
.mode = selection.mode;
685 result.pipelines =
std::move(selection.pipelines);
686 result.diagnostics =
std::move(selection.diagnostics);
690 DiagnosticReporter reporter(result.diagnostics);
691 result.root = Config::overrideOutputDir.isNull() ? config.get(
CONFIG_OUTPUTDIR).asString()
692 : Config::overrideOutputDir;
694 const QString moduleDir = project.asString().toLower();
695 for (qsizetype i = 0; i < selection.formats.size(); ++i) {
697 static_cast<OutputSelection::Format &>(plan) = std::move(selection.formats[i]);
698 const QString subKey = configKey(plan.format, u"outputsubdir"_s);
699 if (config.contains(subKey)) {
700 const auto &sub = config.get(subKey);
701 const auto values = sub.asStringList();
702 if (values.size() > 1)
704 "%1 must contain a single value."_L1.arg(subKey),
705 u"Use one path per format."_s);
706 plan.formatDir = values.value(0);
707 plan.formatDirLocation = sub.location();
709 plan.formatDir = plan.configured && i == 0 ? QString() : plan.format.toLower();
710 plan.formatDirLocation = plan.location;
715 if (config.contains(layoutKey)) {
716 const auto &value = config.get(layoutKey);
717 plan.layoutLocation = value.location();
718 if (
const auto layout = parsePublicationLayout(value.asString())) {
722 "Invalid %1 '%2'."_L1.arg(layoutKey, value.asString()),
723 u"Expected 'flat' or 'per-module'."_s);
726 const QString aliasKey = configKey(plan.format, u"nosubdirs"_s);
727 if (config.contains(aliasKey)) {
728 const auto &alias = config.get(aliasKey);
730 plan.layoutLocation = alias.location();
732 "%1 is deprecated; use %2 or publicationlayout instead."_L1
733 .arg(aliasKey, perKey));
737 if (isValidModuleDir(moduleDir)) {
738 plan.moduleDir = moduleDir;
739 }
else if (plan.requested) {
741 "Invalid project identity '%1' for per-module publication."_L1
742 .arg(project.asString()),
743 u"Project must be a safe directory name."_s);
746 result.formats.push_back(
std::move(plan));
749 QHash<QString,
const FormatPlan *> destinations;
750 for (
const auto &plan : result.formats) {
753 const QString dir = result.destination(plan);
754 const auto it = destinations.constFind(dir);
755 if (it == destinations.constEnd()) {
756 destinations.insert(dir, &plan);
758 const auto &otherLocation = it.value()->formatDirLocation;
759 reporter.add(Diagnostic::Kind::Error, plan.formatDirLocation,
760 "Conflicting output destinations for '%1' and '%2': %3"_L1
761 .arg(it.value()->format, plan.format, dir),
762 otherLocation.isEmpty() ? QString()
763 :
"Other destination declared at %1."_L1.arg(otherLocation.toString()));