From 0322f049cacc27e0115fc06b5ff3d82f69ca64eb Mon Sep 17 00:00:00 2001
From: Jacob Rockowitz <21160-jrockowitz@users.noreply.drupalcode.org>
Date: Thu, 30 May 2024 09:33:48 +0000
Subject: [PATCH] Issue #3450453: Improve support for Google structured data

---
 .cspell-project-words.txt                     |   1 +
 ...adotorg.schemadotorg_mapping_type.node.yml |   1 +
 config/install/schemadotorg.settings.yml      |  27 +++++++-
 docs/DECISIONS.md                             |  15 +++++
 ...emadotorg_additional_mappings.settings.yml |  36 ++++++----
 .../SchemaDotOrgAdditionalMappingsManager.php |  62 +++++++++---------
 .../schemadotorg_custom_field.settings.yml    |  29 ++++++++
 ...ng_values_autocomplete_widget.settings.yml |  17 +++--
 .../schemadotorg_field_group.settings.yml     |  19 +-----
 ...maDotOrgFieldGroupEntityDisplayBuilder.php |   8 ++-
 .../Form/SchemaDotOrgJsonLdSettingsForm.php   |   2 +-
 .../src/SchemaDotOrgJsonLdBuilder.php         |  12 +++-
 .../SchemaDotOrgJsonLdRangeKernelTest.php     |   4 +-
 .../SchemaDotOrgJsonLdBuilderKernelTest.php   |  11 +++-
 .../schemadotorg_mapping_set.settings.yml     |   6 ++
 .../install/schemadotorg_options.settings.yml |  57 ++++++++++++++--
 .../install/schemadotorg_report.settings.yml  |   3 +
 ...entity_form_display.node.event.default.yml |   2 +-
 ...ntity_form_display.node.person.default.yml |   4 +-
 ...entity_view_display.node.event.default.yml |   2 +-
 ...ntity_view_display.node.person.default.yml |   4 +-
 .../schemadotorg_type_tray/icon/vehicle.png   | Bin 0 -> 1288 bytes
 schemadotorg.schemadotorg.inc                 |  13 +++-
 23 files changed, 248 insertions(+), 87 deletions(-)
 create mode 100644 modules/schemadotorg_type_tray/images/schemadotorg_type_tray/icon/vehicle.png

diff --git a/.cspell-project-words.txt b/.cspell-project-words.txt
index 7031276cc..44ed3fac8 100644
--- a/.cspell-project-words.txt
+++ b/.cspell-project-words.txt
@@ -279,3 +279,4 @@ Lullabot
 coinvestigator
 SSML
 GAEP
+CEFACT
diff --git a/config/install/schemadotorg.schemadotorg_mapping_type.node.yml b/config/install/schemadotorg.schemadotorg_mapping_type.node.yml
index 203ba6436..705824873 100644
--- a/config/install/schemadotorg.schemadotorg_mapping_type.node.yml
+++ b/config/install/schemadotorg.schemadotorg_mapping_type.node.yml
@@ -55,6 +55,7 @@ recommended_schema_types:
       - EducationalOrganization
       - CourseInstance
       - Course
+      - Quiz
   food:
     label: Food
     types:
diff --git a/config/install/schemadotorg.settings.yml b/config/install/schemadotorg.settings.yml
index e577e71b7..26193590d 100644
--- a/config/install/schemadotorg.settings.yml
+++ b/config/install/schemadotorg.settings.yml
@@ -148,6 +148,7 @@ schema_types:
       - text
     LearningResource:
       - assesses
+      - educationalAlignment
       - educationalLevel
       - educationalUse
       - learningResourceType
@@ -158,7 +159,10 @@ schema_types:
       - courseCode
       - coursePrerequisites
       - educationalCredentialAwarded
+      - hasCourseInstance
       - numberOfCredits
+    Quiz:
+      - hasPart
     MediaObject:
       - name
     AudioObject:
@@ -298,6 +302,7 @@ schema_types:
       - startDate
     CourseInstance:
       - courseMode
+      - courseSchedule
       - courseWorkload
       - instructor
     PublicationEvent:
@@ -336,6 +341,7 @@ schema_types:
       - name
     JobPosting:
       - applicationContact
+      - baseSalary
       - benefits
       - datePosted
       - description
@@ -343,7 +349,6 @@ schema_types:
       - eligibilityToWorkRequirement
       - employerOverview
       - employmentType
-      - estimatedSalary
       - experienceRequirements
       - jobBenefits
       - jobLocation
@@ -373,6 +378,12 @@ schema_types:
       - author
       - ratingExplanation
       - ratingValue
+    Schedule:
+      - duration
+      - repeatFrequency
+      - repeatCount
+      - startDate
+      - endDate
     Service:
       - availableChannel
       - description
@@ -660,6 +671,7 @@ schema_types:
       - accelerationTime
       - bodyType
       - cargoVolume
+      - color
       - driveWheelConfiguration
       - fuelCapacity
       - fuelConsumption
@@ -671,8 +683,12 @@ schema_types:
       - speed
       - vehicleConfiguration
       - vehicleEngine
+      - vehicleInteriorColor
+      - vehicleInteriorType
       - vehicleModelDate
       - vehicleSeatingCapacity
+      - vehicleSeatingCapacity
+      - vehicleTransmission
     PronounceableText:
       - inLanguage
       - phoneticText
@@ -799,6 +815,8 @@ schema_properties:
       - Accommodation
     Accommodation--containedInPlace:
       - LodgingBusiness
+    bodyType:
+      - Text
     employee:
       - Person
     itemListElement:
@@ -1038,6 +1056,7 @@ schema_properties:
     cookingMethod:
       unlimited: true
     courseMode:
+      label: 'Course modes'
       type: string
       unlimited: true
     coursePrerequisites:
@@ -1060,8 +1079,6 @@ schema_properties:
       unlimited: true
     educationRequirements:
       unlimited: true
-    educationalLevel:
-      label: Difficulty
     educationalUse:
       label: 'Educational uses'
       unlimited: true
@@ -1075,6 +1092,8 @@ schema_properties:
     employee:
       label: Employees
       unlimited: true
+    employmentType:
+      unlimited: true
     employerOverview:
       type: text_long
     epidemiology:
@@ -1349,6 +1368,8 @@ schema_properties:
     supply:
       label: Supplies
       unlimited: true
+    syllabusSections:
+      unlimited: true
     telephone:
       type: telephone
     teaches:
diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md
index 1586155e6..809544f34 100644
--- a/docs/DECISIONS.md
+++ b/docs/DECISIONS.md
@@ -113,6 +113,21 @@ non-function decisions behind the Schema.org Blueprints module.
 - The main/primary mapping is the equivalent the https://schema.org/mainEntityOfPage.
 - All public facing content (a.k.a. nodes) should have https://schema.org/WebPage as an additional mapping.
 
+##### Support [Google Structured Data](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data)
+- Provide reasonable initial support for the common Google Structured Data types.
+- Support the below Schema.org types 
+  - [Article](https://developers.google.com/search/docs/appearance/structured-data/article)
+  - [BreadCrumb](https://developers.google.com/search/docs/appearance/structured-data/breadcrumb)
+  - [Course](https://developers.google.com/search/docs/appearance/structured-data/course-info)
+  - [FAQ](https://developers.google.com/search/docs/appearance/structured-data/faqpage)
+  - [JobPosting](https://developers.google.com/search/docs/appearance/structured-data/job-posting)
+  - [LocalBusiness](https://developers.google.com/search/docs/appearance/structured-data/local-business)
+  - [Organization](https://developers.google.com/search/docs/appearance/structured-data/organization)
+  - [ProfilePage](https://developers.google.com/search/docs/appearance/structured-data/profile-page)
+  - [Vehicle](https://developers.google.com/search/docs/appearance/structured-data/vehicle-listing)
+  - [Quiz](https://developers.google.com/search/docs/appearance/structured-data/education-qa)
+- Support the below Schema.org properties
+  - [image](https://developers.google.com/search/docs/appearance/structured-data/article) 1x1, 4x3, and 16x9 sizes. (@see schemadotorg_demo_standard.module)
 
 # 4000 - User experience
 
diff --git a/modules/schemadotorg_additional_mappings/config/install/schemadotorg_additional_mappings.settings.yml b/modules/schemadotorg_additional_mappings/config/install/schemadotorg_additional_mappings.settings.yml
index 91a31fc6f..6dc2a3c23 100644
--- a/modules/schemadotorg_additional_mappings/config/install/schemadotorg_additional_mappings.settings.yml
+++ b/modules/schemadotorg_additional_mappings/config/install/schemadotorg_additional_mappings.settings.yml
@@ -1,23 +1,29 @@
 default_additional_mappings:
-  node--CreativeWork:
-    - WebPage
+  node--IndividualPhysician:
+    - ProfilePage
+    - Person
+  node--MedicalCondition:
+    - MedicalWebPage
+    - HealthTopicContent
+  node--MedicalStudy:
+    - MedicalWebPage
+    - ResearchProject
+  node--Substance:
+    - MedicalWebPage
+    - CreativeWork
+    - PronounceableText
   node--HealthTopicContent:
     - MedicalWebPage
+  node--CreativeWork:
+    - WebPage
   node--Organization:
     - WebPage
   node--Place:
     - WebPage
+  node--Person:
+    - ProfilePage
   node--MedicalEntity:
     - MedicalWebPage
-  node--MedicalCondition:
-    - HealthTopicContent
-  node--MedicalStudy:
-    - ResearchProject
-  node--Substance:
-    - CreativeWork
-    - PronounceableText
-  node--IndividualPhysician:
-    - Person
 default_properties:
   CreativeWork:
     - citation
@@ -47,6 +53,14 @@ default_properties:
     - relatedLink
     - significantLink
     - medicalAudience
+  ProfilePage:
+    - dateCreated
+    - dateModified
+    - inLanguage
+    - name
+    - primaryImageOfPage
+    - relatedLink
+    - significantLink
   ResearchProject:
     - member
   Person:
diff --git a/modules/schemadotorg_additional_mappings/src/SchemaDotOrgAdditionalMappingsManager.php b/modules/schemadotorg_additional_mappings/src/SchemaDotOrgAdditionalMappingsManager.php
index 17b7aa6d1..e9592143e 100644
--- a/modules/schemadotorg_additional_mappings/src/SchemaDotOrgAdditionalMappingsManager.php
+++ b/modules/schemadotorg_additional_mappings/src/SchemaDotOrgAdditionalMappingsManager.php
@@ -343,7 +343,7 @@ class SchemaDotOrgAdditionalMappingsManager implements SchemaDotOrgAdditionalMap
       'schema_type' => $schema_type,
     ];
 
-    $additional_mapping_types = $this->schemaTypeManager->getSetting($default_additional_mappings, $setting_parts, TRUE);
+    $additional_mapping_types = $this->schemaTypeManager->getSetting($default_additional_mappings, $setting_parts);
     if (!$additional_mapping_types) {
       return [];
     }
@@ -351,44 +351,42 @@ class SchemaDotOrgAdditionalMappingsManager implements SchemaDotOrgAdditionalMap
     $additional_mappings = [];
 
     $has_webpage_schema_type = FALSE;
-    foreach ($additional_mapping_types as $additional_mapping_type) {
-      foreach ($additional_mapping_type as $additional_schema_type) {
-        if ($this->schemaTypeManager->isSubTypeOf($additional_schema_type, $schema_type)) {
-          continue;
-        }
+    foreach ($additional_mapping_types as $additional_schema_type) {
+      if ($this->schemaTypeManager->isSubTypeOf($additional_schema_type, $schema_type)) {
+        continue;
+      }
 
-        // Limit additional mapping Schema.org types to one type of
-        // https://schema.org/WebPage.
-        if ($this->schemaTypeManager->isSubTypeOf($additional_schema_type, 'WebPage')) {
-          if ($has_webpage_schema_type) {
-            continue;
-          }
-          $has_webpage_schema_type = TRUE;
+      // Limit additional mapping Schema.org types to one type of
+      // https://schema.org/WebPage.
+      if ($this->schemaTypeManager->isSubTypeOf($additional_schema_type, 'WebPage')) {
+        if ($has_webpage_schema_type) {
+          continue;
         }
+        $has_webpage_schema_type = TRUE;
+      }
 
-        $mapping_defaults = $this->schemaMappingManager->getMappingDefaults(
-          entity_type_id: $entity_type_id,
-          schema_type: $additional_schema_type,
-        );
+      $mapping_defaults = $this->schemaMappingManager->getMappingDefaults(
+        entity_type_id: $entity_type_id,
+        schema_type: $additional_schema_type,
+      );
 
-        $default_properties = $this->getDefaultProperties($schema_type, $additional_schema_type);
-        $schema_properties = [];
-        foreach ($mapping_defaults['properties'] as $schema_property => $property) {
-          if (isset($default_properties[$schema_property])) {
-            $field_name = $property['name'];
-            // Make sure the field is set if it does not already exist.
-            if (empty($field_name)
-              || $field_name === SchemaDotOrgEntityFieldManagerInterface::ADD_FIELD) {
-              $field_name = $this->schemaNames->getFieldPrefix() . $property['machine_name'];
-            }
-            $schema_properties[$field_name] = $schema_property;
+      $default_properties = $this->getDefaultProperties($schema_type, $additional_schema_type);
+      $schema_properties = [];
+      foreach ($mapping_defaults['properties'] as $schema_property => $property) {
+        if (isset($default_properties[$schema_property])) {
+          $field_name = $property['name'];
+          // Make sure the field is set if it does not already exist.
+          if (empty($field_name)
+            || $field_name === SchemaDotOrgEntityFieldManagerInterface::ADD_FIELD) {
+            $field_name = $this->schemaNames->getFieldPrefix() . $property['machine_name'];
           }
+          $schema_properties[$field_name] = $schema_property;
         }
-        $additional_mappings[$additional_schema_type] = [
-          'schema_type' => $additional_schema_type,
-          'schema_properties' => $schema_properties,
-        ];
       }
+      $additional_mappings[$additional_schema_type] = [
+        'schema_type' => $additional_schema_type,
+        'schema_properties' => $schema_properties,
+      ];
     }
     return $additional_mappings;
   }
diff --git a/modules/schemadotorg_custom_field/config/install/schemadotorg_custom_field.settings.yml b/modules/schemadotorg_custom_field/config/install/schemadotorg_custom_field.settings.yml
index 5815d77b7..8bed932c0 100644
--- a/modules/schemadotorg_custom_field/config/install/schemadotorg_custom_field.settings.yml
+++ b/modules/schemadotorg_custom_field/config/install/schemadotorg_custom_field.settings.yml
@@ -26,6 +26,16 @@ default_schema_properties:
       doseValue: integer
       doseUnit: string
       frequency: string
+  educationRequirements:
+    type: EducationalOccupationalCredential
+    properties:
+      description: string
+      credentialCategory: string
+  experienceRequirements:
+    type: OccupationalExperienceRequirements
+    properties:
+      description: string
+      monthsOfExperience: integer
   recommendedIntake:
     type: RecommendedDoseSchedule
     properties:
@@ -33,6 +43,19 @@ default_schema_properties:
       doseValue: decimal
       doseUnit: string
       frequency: string
+  educationalAlignment:
+    type: AlignmentObject
+    properties:
+      alignmentType: string
+      targetName: string
+      targetDescription: string
+      educationalFramework: string
+      targetUrl: url
+  syllabusSections:
+    type: Syllabus
+    properties:
+      name: string
+      description: string_long
   maximumIntake:
     type: DoseSchedule
     properties:
@@ -50,4 +73,10 @@ default_schema_properties:
     properties:
       name: string_long
       acceptedAnswer: string_long
+  Quiz--hasPart:
+    type: Question
+    properties:
+      name: string_long
+      eduQuestionType: string
+      acceptedAnswer: string_long
 default_format: basic_html
diff --git a/modules/schemadotorg_existing_values_autocomplete_widget/config/install/schemadotorg_existing_values_autocomplete_widget.settings.yml b/modules/schemadotorg_existing_values_autocomplete_widget/config/install/schemadotorg_existing_values_autocomplete_widget.settings.yml
index f62272023..b72c3a7fa 100644
--- a/modules/schemadotorg_existing_values_autocomplete_widget/config/install/schemadotorg_existing_values_autocomplete_widget.settings.yml
+++ b/modules/schemadotorg_existing_values_autocomplete_widget/config/install/schemadotorg_existing_values_autocomplete_widget.settings.yml
@@ -2,13 +2,15 @@ default_schema_properties:
   - activeIngredient
   - alumniOf
   - birthPlace
+  - color
   - coursePrerequisites
+  - courseWorkload
   - dateline
   - dietFeatures
   - doseUnit
   - drugClass
   - drugUnit
-  - educationRequirement
+  - educationRequirements
   - educationalCredentialAwarded
   - educationalProgramMode
   - educationalUse
@@ -16,13 +18,14 @@ default_schema_properties:
   - employmentType
   - endorsers
   - expectedPrognosis
-  - experienceRequirement
+  - experienceRequirements
   - expertConsiderations
   - frequency
   - hospitalAffiliation
   - industry
   - jobLocationType
   - jobTitle
+  - jobBenefits
   - keywords
   - learningResourceType
   - naturalProgression
@@ -34,8 +37,8 @@ default_schema_properties:
   - primaryPrevention
   - programPrerequisite
   - programType
-  - qualification
-  - responsibility
+  - qualifications
+  - responsibilities
   - riskFactor
   - risks
   - secondaryPrevention
@@ -46,3 +49,9 @@ default_schema_properties:
   - teaches
   - timeOfDay
   - typicalTest
+  - vehicleConfiguration
+  - vehicleEngine
+  - vehicleInteriorColor
+  - vehicleInteriorType
+  - vehicleSeatingCapacity
+  - vehicleTransmission
diff --git a/modules/schemadotorg_field_group/config/install/schemadotorg_field_group.settings.yml b/modules/schemadotorg_field_group/config/install/schemadotorg_field_group.settings.yml
index f3868c7b7..90a7e08a5 100644
--- a/modules/schemadotorg_field_group/config/install/schemadotorg_field_group.settings.yml
+++ b/modules/schemadotorg_field_group/config/install/schemadotorg_field_group.settings.yml
@@ -85,6 +85,7 @@ default_field_groups:
         - responsibilities
         - skills
         - educationRequirements
+        - baseSalary
         - estimatedSalary
         - jobBenefits
         - jobLocation
@@ -92,23 +93,6 @@ default_field_groups:
         - eligibilityToWorkRequirement
         - employerOverview
         - applicationContact
-    education:
-      label: Education
-      properties:
-        - instructor
-        - teaches
-        - assesses
-        - numberOfCredits
-        - educationalLevel
-        - courseCode
-        - courseMode
-        - coursePrerequisites
-        - courseWorkload
-        - educationalCredentialAwarded
-        - occupationalCredentialAwarded
-        - educationalUse
-        - learningResourceType
-        - hasCourseInstance
     contact:
       label: Contact
       properties:
@@ -209,6 +193,7 @@ default_field_groups:
         - partOfSeason
         - containsSeason
         - partOfSeries
+        - hasCourseInstance
     references:
       label: References
       properties:
diff --git a/modules/schemadotorg_field_group/src/SchemaDotOrgFieldGroupEntityDisplayBuilder.php b/modules/schemadotorg_field_group/src/SchemaDotOrgFieldGroupEntityDisplayBuilder.php
index 627828ad7..e16aae1c8 100644
--- a/modules/schemadotorg_field_group/src/SchemaDotOrgFieldGroupEntityDisplayBuilder.php
+++ b/modules/schemadotorg_field_group/src/SchemaDotOrgFieldGroupEntityDisplayBuilder.php
@@ -201,9 +201,11 @@ class SchemaDotOrgFieldGroupEntityDisplayBuilder implements SchemaDotOrgFieldGro
       $group_label = $default_field_groups[$group_name]['label'];
       $default_group_weights = [
         'links' => 10 + $max_group_weight,
-        'relationships'  => 20 + $max_group_weight,
-        'taxonomy' => 30 + $max_group_weight,
-        'identifiers' => 40 + $max_group_weight,
+        'hierarchy' => 20 + $max_group_weight,
+        'references' => 30 + $max_group_weight,
+        'relationships' => 40 + $max_group_weight,
+        'taxonomy' => 50 + $max_group_weight,
+        'identifiers' => 60 + $max_group_weight,
       ];
       $group_weight = $default_group_weights[$group_name]
         ?? $group_weights[$group_name]
diff --git a/modules/schemadotorg_jsonld/src/Form/SchemaDotOrgJsonLdSettingsForm.php b/modules/schemadotorg_jsonld/src/Form/SchemaDotOrgJsonLdSettingsForm.php
index d890a55f8..344125377 100644
--- a/modules/schemadotorg_jsonld/src/Form/SchemaDotOrgJsonLdSettingsForm.php
+++ b/modules/schemadotorg_jsonld/src/Form/SchemaDotOrgJsonLdSettingsForm.php
@@ -63,7 +63,7 @@ class SchemaDotOrgJsonLdSettingsForm extends SchemaDotOrgSettingsFormBase {
       '#example' => "
 Intangible: entity
 node--Thing: url
-media--image: '[media:field_media_image:entity:url]'
+media--image: '[media:field_media_image:1x1:url], [media:field_media_image:4x3:url], [media:field_media_image:16x9:url]'
 paragraph--layout: none
 ",
     ];
diff --git a/modules/schemadotorg_jsonld/src/SchemaDotOrgJsonLdBuilder.php b/modules/schemadotorg_jsonld/src/SchemaDotOrgJsonLdBuilder.php
index 392de9c24..fa76d5228 100644
--- a/modules/schemadotorg_jsonld/src/SchemaDotOrgJsonLdBuilder.php
+++ b/modules/schemadotorg_jsonld/src/SchemaDotOrgJsonLdBuilder.php
@@ -386,7 +386,17 @@ class SchemaDotOrgJsonLdBuilder implements SchemaDotOrgJsonLdBuilderInterface {
           if (str_starts_with($entity_reference_display, '[')
             && str_ends_with($entity_reference_display, ']')) {
             $data = [$target_entity->getEntityTypeId() => $target_entity];
-            return $this->token->replace($entity_reference_display, $data, [], $bubbleable_metadata);
+            // Split tokens delimited by commas into multiple values.
+            if (preg_match('/]\s*,\s*\[/', $entity_reference_display)) {
+              $values = preg_split('/(?<=\])\s*,\s*(?=\[)/', $entity_reference_display);
+              foreach ($values as $index => $value) {
+                $values[$index] = $this->token->replace($value, $data, [], $bubbleable_metadata);
+              }
+              return $values;
+            }
+            else {
+              return $this->token->replace($entity_reference_display, $data, [], $bubbleable_metadata);
+            }
           }
           else {
             return $target_entity->label();
diff --git a/modules/schemadotorg_jsonld/tests/src/Kernel/Modules/SchemaDotOrgJsonLdRangeKernelTest.php b/modules/schemadotorg_jsonld/tests/src/Kernel/Modules/SchemaDotOrgJsonLdRangeKernelTest.php
index bd531f925..ae93bab9b 100644
--- a/modules/schemadotorg_jsonld/tests/src/Kernel/Modules/SchemaDotOrgJsonLdRangeKernelTest.php
+++ b/modules/schemadotorg_jsonld/tests/src/Kernel/Modules/SchemaDotOrgJsonLdRangeKernelTest.php
@@ -50,7 +50,7 @@ class SchemaDotOrgJsonLdRangeKernelTest extends SchemaDotOrgEntityKernelTestBase
     $job_node = Node::create([
       'type' => 'job_posting',
       'title' => 'Some job',
-      'schema_estimated_salary' => [
+      'schema_base_salary' => [
         'from' => 100000,
         'to' => 200000,
       ],
@@ -61,7 +61,7 @@ class SchemaDotOrgJsonLdRangeKernelTest extends SchemaDotOrgEntityKernelTestBase
       '@type' => 'JobPosting',
       '@url' => $job_node->toUrl()->setAbsolute()->toString(),
       'title' => 'Some job',
-      'estimatedSalary' => [
+      'baseSalary' => [
         '@type' => 'MonetaryAmount',
         'minValue' => 100000,
         'maxValue' => 200000,
diff --git a/modules/schemadotorg_jsonld/tests/src/Kernel/SchemaDotOrgJsonLdBuilderKernelTest.php b/modules/schemadotorg_jsonld/tests/src/Kernel/SchemaDotOrgJsonLdBuilderKernelTest.php
index 66844a4a4..9a46a990d 100644
--- a/modules/schemadotorg_jsonld/tests/src/Kernel/SchemaDotOrgJsonLdBuilderKernelTest.php
+++ b/modules/schemadotorg_jsonld/tests/src/Kernel/SchemaDotOrgJsonLdBuilderKernelTest.php
@@ -93,6 +93,7 @@ class SchemaDotOrgJsonLdBuilderKernelTest extends SchemaDotOrgJsonLdKernelTestBa
     ]);
     $creative_work_node->save();
 
+    $expected_image_uri = \Drupal::service('file_url_generator')->generateAbsoluteString($file->getFileUri());
     // Check building JSON-LD for an entity that is mapped to a Schema.org type.
     $expected_result = [
       '@type' => 'CreativeWork',
@@ -103,7 +104,7 @@ class SchemaDotOrgJsonLdBuilderKernelTest extends SchemaDotOrgJsonLdKernelTestBa
       ],
       'description' => 'A summary',
       'text' => 'Some description',
-      'image' => \Drupal::service('file_url_generator')->generateAbsoluteString($file->getFileUri()),
+      'image' => $expected_image_uri,
       'subjectOf' => [
         [
           '@type' => 'CreativeWork',
@@ -116,6 +117,14 @@ class SchemaDotOrgJsonLdBuilderKernelTest extends SchemaDotOrgJsonLdKernelTestBa
     ];
     $this->assertEquals($expected_result, $this->builder->buildEntity($creative_work_node));
 
+    // Check that multiple tokens are split into multiple values.
+    // This is used to support multiple image styles.
+    $this->config('schemadotorg_jsonld.settings')
+      ->set('schema_type_entity_references_display.media--image', '[media:field_media_image:entity:url], [media:field_media_image:entity:url]')
+      ->save();
+    $jsonld = $this->builder->buildEntity($creative_work_node);
+    $this->assertEquals([$expected_image_uri, $expected_image_uri], $jsonld['image']);
+
     /* ********************************************************************* */
 
     // Set relatedLink to use an entity reference field instead of a link field.
diff --git a/modules/schemadotorg_mapping_set/config/install/schemadotorg_mapping_set.settings.yml b/modules/schemadotorg_mapping_set/config/install/schemadotorg_mapping_set.settings.yml
index 2c81ba70d..e59818e17 100644
--- a/modules/schemadotorg_mapping_set/config/install/schemadotorg_mapping_set.settings.yml
+++ b/modules/schemadotorg_mapping_set/config/install/schemadotorg_mapping_set.settings.yml
@@ -46,6 +46,12 @@ sets:
       - 'node:Menu'
       - 'node:Recipe'
       - 'node:FoodEstablishment'
+  course:
+    label: Course
+    types:
+      - 'paragraph:Schedule'
+      - 'node:CourseInstance'
+      - 'node:Course'
   movie:
     label: Movie
     types:
diff --git a/modules/schemadotorg_options/config/install/schemadotorg_options.settings.yml b/modules/schemadotorg_options/config/install/schemadotorg_options.settings.yml
index 8979eee10..6b20a6bbe 100644
--- a/modules/schemadotorg_options/config/install/schemadotorg_options.settings.yml
+++ b/modules/schemadotorg_options/config/install/schemadotorg_options.settings.yml
@@ -1,4 +1,16 @@
 schema_property_allowed_values:
+  contactType:
+    Home: Home
+    Work: Work
+    Cell: Cell
+  courseMode:
+    Online: Online
+    Onsite: Onsite
+    Blended: Blended
+    Synchronous: Synchronous
+    Asynchronous: Asynchronous
+    Full-time: Full-time
+    Part-time: Part-time
   difficulty:
     easy: Easy
     medium: Medium
@@ -70,10 +82,19 @@ schema_property_allowed_values:
   doseUnit:
     gram: gram
     milligram: milligram
-  contactType:
-    Home: Home
-    Work: Work
-    Cell: Cell
+  educationalLevel:
+    Beginner: Beginner
+    Intermediate: Intermediate
+    Advanced: Advanced
+  employmentType:
+    FULL_TIME: Full-time
+    PART_TIME: Part-time
+    CONTRACTOR: Contractor
+    TEMPORARY: Temporary
+    INTERN: Internship
+    VOLUNTEER: Volunteer
+    PER_DIEM: Per diem
+    OTHER: Other
   frequency:
     Daily: Daily
     '2 times a day': '2 times a day'
@@ -100,6 +121,24 @@ schema_property_allowed_values:
     MD: M.D.
     PHD: PhD
     MSCSW: MSCSW
+  jobLocationType:
+    On-site: On-site
+    Hybrid: Hybrid
+    Remote: Remote
+  learningResourceType:
+    Books: Books
+    Classroom: Classroom
+    Digital: Digital
+    Discussion: Discussion
+    Film: Film
+    Lectures: Lectures
+    Lesson: Lesson
+    Performances: Performances
+    Plays: Plays
+    Podcasts: Podcasts
+    'Social media': 'Social media'
+    Software: Software
+    Textbook: Textbook
   priceRange:
     '$': $
     '$$': $$
@@ -123,6 +162,16 @@ schema_property_allowed_values:
     double: Double
     semi_double: 'Semi double'
     single: Single
+  Car--bodyType:
+    convertible: Convertible
+    coupe: Coupe
+    crossover: Crossover
+    'full size van': Full size van
+    hatchback: Hatchback
+    minivan: Minivan
+    sedan: Sedan
+    suv: SYV
+    truck: Truck
   SpecialAnnouncement--category:
     information: Information
     condition: Condition
diff --git a/modules/schemadotorg_report/config/install/schemadotorg_report.settings.yml b/modules/schemadotorg_report/config/install/schemadotorg_report.settings.yml
index 3b9e6701f..8688547eb 100644
--- a/modules/schemadotorg_report/config/install/schemadotorg_report.settings.yml
+++ b/modules/schemadotorg_report/config/install/schemadotorg_report.settings.yml
@@ -203,6 +203,9 @@ types:
   Quiz:
     - title: 'Practice Problems Structured Data'
       uri: 'https://developers.google.com/search/docs/advanced/structured-data/practice-problems'
+  QuantitativeValue:
+    - title: 'Using UN CEFACT Codes'
+      uri: 'https://github.com/schemaorg/schemaorg/wiki/Using-UN-CEFACT-Codes'
   Recipe:
     - title: 'Recipe Schema Markup'
       uri: 'https://developers.google.com/search/docs/advanced/structured-data/recipe'
diff --git a/modules/schemadotorg_starterkit/tests/schemadotorg/config/snapshot/core.entity_form_display.node.event.default.yml b/modules/schemadotorg_starterkit/tests/schemadotorg/config/snapshot/core.entity_form_display.node.event.default.yml
index 4ecb0d81e..a1bad3a31 100644
--- a/modules/schemadotorg_starterkit/tests/schemadotorg/config/snapshot/core.entity_form_display.node.event.default.yml
+++ b/modules/schemadotorg_starterkit/tests/schemadotorg/config/snapshot/core.entity_form_display.node.event.default.yml
@@ -33,7 +33,7 @@ third_party_settings:
       label: Event
       region: content
       parent_name: ''
-      weight: -1
+      weight: -2
       format_type: details
       format_settings:
         open: true
diff --git a/modules/schemadotorg_starterkit/tests/schemadotorg/config/snapshot/core.entity_form_display.node.person.default.yml b/modules/schemadotorg_starterkit/tests/schemadotorg/config/snapshot/core.entity_form_display.node.person.default.yml
index c726828ec..c96e26ef4 100644
--- a/modules/schemadotorg_starterkit/tests/schemadotorg/config/snapshot/core.entity_form_display.node.person.default.yml
+++ b/modules/schemadotorg_starterkit/tests/schemadotorg/config/snapshot/core.entity_form_display.node.person.default.yml
@@ -46,7 +46,7 @@ third_party_settings:
       label: Contact
       region: content
       parent_name: ''
-      weight: -2
+      weight: -3
       format_type: details
       format_settings:
         open: true
@@ -57,7 +57,7 @@ third_party_settings:
       label: Organization
       region: content
       parent_name: ''
-      weight: 4
+      weight: 3
       format_type: details
       format_settings:
         open: true
diff --git a/modules/schemadotorg_starterkit/tests/schemadotorg/config/snapshot/core.entity_view_display.node.event.default.yml b/modules/schemadotorg_starterkit/tests/schemadotorg/config/snapshot/core.entity_view_display.node.event.default.yml
index 805891a83..81c64f15d 100644
--- a/modules/schemadotorg_starterkit/tests/schemadotorg/config/snapshot/core.entity_view_display.node.event.default.yml
+++ b/modules/schemadotorg_starterkit/tests/schemadotorg/config/snapshot/core.entity_view_display.node.event.default.yml
@@ -32,7 +32,7 @@ third_party_settings:
       label: Event
       parent_name: ''
       region: content
-      weight: -1
+      weight: -2
       format_type: fieldset
       format_settings: {  }
 id: node.event.default
diff --git a/modules/schemadotorg_starterkit/tests/schemadotorg/config/snapshot/core.entity_view_display.node.person.default.yml b/modules/schemadotorg_starterkit/tests/schemadotorg/config/snapshot/core.entity_view_display.node.person.default.yml
index cf3b8b04a..f478f8c7a 100644
--- a/modules/schemadotorg_starterkit/tests/schemadotorg/config/snapshot/core.entity_view_display.node.person.default.yml
+++ b/modules/schemadotorg_starterkit/tests/schemadotorg/config/snapshot/core.entity_view_display.node.person.default.yml
@@ -44,7 +44,7 @@ third_party_settings:
       label: Contact
       parent_name: ''
       region: content
-      weight: -2
+      weight: -3
       format_type: fieldset
       format_settings: {  }
     group_organization:
@@ -54,7 +54,7 @@ third_party_settings:
       label: Organization
       parent_name: ''
       region: content
-      weight: 4
+      weight: 3
       format_type: fieldset
       format_settings: {  }
     group_links:
diff --git a/modules/schemadotorg_type_tray/images/schemadotorg_type_tray/icon/vehicle.png b/modules/schemadotorg_type_tray/images/schemadotorg_type_tray/icon/vehicle.png
new file mode 100644
index 0000000000000000000000000000000000000000..e4794a962b7342435a2d910c1d0eafeca4f5ac68
GIT binary patch
literal 1288
zcmeAS@N?(olHy`uVBq!ia0y~yU`PRB4mJh`hJr^^Ll_ts7>k44ofy`glX=O&z;ejb
z#WAEJ?(N*$rMC@u+K%d6OKZRJdP}7GIlfYH<LFm2>^80C*0WdiXp8?O(8=<jpTV<Z
zQ?R&vIG0NJwHCuzhQenoj4BNl!3Snq3Mw)lnaOnSOeY7Y!a0$GG(Q&)hQehH#>Glb
zOec0Q$;->zpRT`m@7}NdVe?NvEz+y4t*`%HfBoMYF~#GLKbA|@ocrm1y>yvCn#=oz
zPv+QPXG-B)(s07^f7ESZ&cE}6z3TVx+gEjb<F6TvHw8M>-v)cl4@;QEtXb^rwbG?!
z`WBVaZ<}4Uvbj=D2O7HSKL2yCs4kN&rQbkn$u!M1oD*yhXYjJ}J+VsvGg0l8?%nH3
zK@S}G+MVxBS`iiPzg#%PIU~9K@Isek*TRE!++JujGFC1P`Rc*L$;$Rbba65V+tghO
z4gwbg_pIUBAo4qE-RE|tYfWlNkGr~rI0Sr47O)-i`Xel^J^l6b%P+sI+v?2gA}l*E
zAw<zMKwx);sX%5)Ma#n0CJAAtj+Fb1ocXy`Gj_*IY2G>+%jz;~oxsAoLT<Uyn@(PJ
z>*VS%h~3%Gm-;HG*Pkcl{l&lq9+#)7F?Te+v9-G96>U4Aotgd0mow`kGi}<lM1EOi
zPWiL`<wn1aSC&uoyr@|H=9VbOZs!ZUlTOB7DvcCSS$(eSxI)>cNsU?WH<m;hD@6N*
zpSn8j2>-b-&Fsw=7tXxH>XOrtV`-(cc*%<^>o-N6SKhis?}J46_APdWnQTX<uBz*O
zdHGka{H0m@;$>?h!m<;#&;9k_#PbVI-=3X6{^3q^;cb&?%y*C8sw%74RHC#tbm#O#
zjg7^k4+9k#7u#-KIrX`QK;pv<e>KXk9gfg^$=Q)QGa>!grgr0LyXG~xlx!#sz578z
zwcH`;T|lPxuAhcp?;1QVg=TJ$%X5~q5$-+rZQ1T}fu+gwSr2)Jd-H!*Ey(>k<vW|)
z@7RZ5^_(OeJg(lB-5X!A*j;yukxY>o$IBkkt1BE|R`IXboG_<AqWs9BwNf0nH`wZk
z?eMEuyC_)ZqMV0epWvZSPwu_&<K#Iuqts!V+7?S+Gp^RTf~(k$e0<fA!KW?2oAc!P
zrnIFO6K8I+I$J3B$VEMP{lQqa<WP5y<(|>&nOY|DesSnh_HwH1Uai|)nA9MmF~?(P
zRnPMG#}=tsT$_H*Kxy%}er6+y!2ceb?D}eCUi!_uuHPH@D(A8TOUcSOvFpCt0uDBn
z?0f#a%PffA^0e@x!M7t5LK5ygdSH8N4~NP#!QdSK)R`-T)df}AVvc>YSnQzWb?fqs
zTZet;#oUSNT)emCf}EQ6iDCy0{_q81`vQ^~k4U~@5OSEK`QXf)Qe~IV6OK=waG=$X
z!Lfl?fkU)0mqlQ~jE-eY98RtY(u^z`YKvBeHY&W1+<2<0i2a`yH-}5jC;o?5Pnb#M
zv!5wDUB|tBOPkfE!<I9fMAo}TYpBn)jxzZzTF<~-CM+vzkRUhnYRX4zannRC%am(}
zHc!mWnp=`-7t0dFrfiT@KGCp6tJ1h<)-F$nRfb0=ES>MR_19keCD*-gFZvUs^ZmM9
xgu{}y<FzHMOdPS=leL*UI?sIZBai)`QN=WhEv2{HpMima!PC{xWt~$(699nrI9~t&

literal 0
HcmV?d00001

diff --git a/schemadotorg.schemadotorg.inc b/schemadotorg.schemadotorg.inc
index d61e816b7..932ca03e4 100644
--- a/schemadotorg.schemadotorg.inc
+++ b/schemadotorg.schemadotorg.inc
@@ -34,8 +34,17 @@ function duration_field_schemadotorg_property_field_alter(
     return;
   }
 
-  // Set duration granularity to hours and minutes.
-  $field_values['settings'] = ['granularity' => 'h:i'];
+  // Set duration granularity for a specific Schema.org property
+  // and default to to hours and minutes.
+  $granularity = [
+    'repeatFrequency' => 'y:m:d:h:i',
+    'gracePeriod' => 'y:m:d:h:i',
+    'leaseLength' => 'y:m:d:h:i',
+    'validFor' => 'y:m:d:h:i',
+  ];
+  $field_values['settings'] = [
+    'granularity' => $granularity[$schema_property] ?? 'h:i',
+  ];
 }
 
 /**
-- 
GitLab