Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Changelog

All notable changes to this project will be documented in this file.

## [1.4.0] - 2026-03-30

### Enhancement

- **Variant utility** — read variant aliases and emit `data-csvariants` JSON (single or many entries).
- **`VariantUtilityError`** on invalid input.

## [1.3.4] - 2025-01-17

### Changed

- Deployment targets updated.
- General enhancements and latest platform support.

## [1.3.3] - 2024-05-17

### Added

- Privacy manifest file.

### Changed

- Updated tests.

## [1.3.2] - 2024-03-28

### Added

- Support for the **fragment** tag in JSON RTE.

## [1.3.1] - 2023-11-20

### Fixed

- Image linking issue.

## [1.3.0] - 2023-05-26

### Added

- Nested asset support.
- Break tag support.

## [1.2.1] - 2022-09-09

### Fixed

- Swift Package warning (exclude warning from package removed).

## [1.2.0] - 2021-08-10

### Added

- JSON RTE to HTML support for the **GQL API**.

## [1.1.2] - 2021-07-16

### Added

- JSON RTE content to HTML parsing support.

## [1.1.1] - 2021-04-09

### Changed

- Deployment target updates.

### Removed

- XC Framework (issue resolved).

## [1.1.0] - 2021-04-06

### Fixed

- Swift Package duplicate naming for ContentstackUtils.

## [1.0.0] - 2021-04-06

### Added

- Embedded items feature support.
- `includeEmbeddedItems` in Entry and Query modules.
- Utils SDK support in the SDK.
14 changes: 14 additions & 0 deletions ContentstackUtils.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@
0FFF2F2A2668FC54003E9DBF /* NodeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFF2F292668FC54003E9DBF /* NodeType.swift */; };
0FFF2F382668FE85003E9DBF /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FFF2F372668FE85003E9DBF /* Node.swift */; };
64F522132BF5F3F300AE6E0F /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 64F522122BF5F3F300AE6E0F /* PrivacyInfo.xcprivacy */; };
6749AC902F714E26007282C5 /* variantsEntries.json in Resources */ = {isa = PBXBuildFile; fileRef = 6749AC8F2F714E26007282C5 /* variantsEntries.json */; };
6749AC922F714E2F007282C5 /* variantsSingleEntry.json in Resources */ = {isa = PBXBuildFile; fileRef = 6749AC912F714E2F007282C5 /* variantsSingleEntry.json */; };
6749AC942F714E36007282C5 /* VariantUtilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6749AC932F714E36007282C5 /* VariantUtilityTests.swift */; };
OBJ_22 /* ContentstackUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* ContentstackUtils.swift */; };
OBJ_29 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; };
OBJ_40 /* ContentstackUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* ContentstackUtilsTests.swift */; };
Expand Down Expand Up @@ -138,6 +141,10 @@
0FFF2F292668FC54003E9DBF /* NodeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeType.swift; sourceTree = "<group>"; };
0FFF2F372668FE85003E9DBF /* Node.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = "<group>"; };
64F522122BF5F3F300AE6E0F /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
6749AC8F2F714E26007282C5 /* variantsEntries.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = variantsEntries.json; sourceTree = "<group>"; };
6749AC912F714E2F007282C5 /* variantsSingleEntry.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = variantsSingleEntry.json; sourceTree = "<group>"; };
6749AC932F714E36007282C5 /* VariantUtilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VariantUtilityTests.swift; sourceTree = "<group>"; };
6749AC952F715507007282C5 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
"ContentstackUtils::ContentstackUtils::Product" /* ContentstackUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ContentstackUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; };
"ContentstackUtils::ContentstackUtilsTests::Product" /* ContentstackUtilsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; path = ContentstackUtilsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
OBJ_12 /* ContentstackUtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentstackUtilsTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -321,6 +328,9 @@
0F07E62E25244DB5003E0BD1 /* StringExtensionTests.swift */,
0F5E484B2525DB600038C16B /* TestClient.swift */,
0FEC0B3A254FEC60008D4E66 /* MetadataTests.swift */,
6749AC8F2F714E26007282C5 /* variantsEntries.json */,
6749AC912F714E2F007282C5 /* variantsSingleEntry.json */,
6749AC932F714E36007282C5 /* VariantUtilityTests.swift */,
0F7142C325514A6F00C18A61 /* ContentstackUtilsArrayTest.swift */,
0F7142C52551684600C18A61 /* ContentstackUtilsCustomRendertest.swift */,
0F579540266A50D40082815C /* MarkTypeTest.swift */,
Expand All @@ -347,6 +357,7 @@
64F522122BF5F3F300AE6E0F /* PrivacyInfo.xcprivacy */,
0FAA3EBD26A1C65B00173FA9 /* ContentstackUtils.podspec */,
OBJ_6 /* Package.swift */,
6749AC952F715507007282C5 /* CHANGELOG.md */,
0F7142C725517A4900C18A61 /* README.md */,
0FA3D58E252228E300E58179 /* Scripts */,
OBJ_7 /* Sources */,
Expand Down Expand Up @@ -471,7 +482,9 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6749AC922F714E2F007282C5 /* variantsSingleEntry.json in Resources */,
64F522132BF5F3F300AE6E0F /* PrivacyInfo.xcprivacy in Resources */,
6749AC902F714E26007282C5 /* variantsEntries.json in Resources */,
0F5E484E2525DDD70038C16B /* EntryEmbedded.json in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -556,6 +569,7 @@
0FFD88D7266DDD1900BA5919 /* ContentstackUtilsJsonToHtmlTest.swift in Sources */,
0F07E62F25244DB5003E0BD1 /* StringExtensionTests.swift in Sources */,
0F7142C425514A6F00C18A61 /* ContentstackUtilsArrayTest.swift in Sources */,
6749AC942F714E36007282C5 /* VariantUtilityTests.swift in Sources */,
0FFD88EE266DE1A600BA5919 /* NodeParser.swift in Sources */,
0FFD88F7266DE1FB00BA5919 /* JsonNodes.swift in Sources */,
0F00785B26A5A0EB00FC4925 /* GQLJsonToHtml.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2016-2025 Contentstack
Copyright (c) 2016-2026 Contentstack

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
78 changes: 78 additions & 0 deletions Sources/ContentstackUtils/ContentstackUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import Foundation

public struct ContentstackUtils {

public enum VariantUtilityError: Error {
case invalidArgument(String)
}

public struct GQL {
public static func jsonToHtml(rte document: [String: Any?], _ option: Option = Option()) throws -> Any {
do {
Expand Down Expand Up @@ -72,6 +76,79 @@ public struct ContentstackUtils {
public static func jsonToHtml(node document: Node, _ option: Option = Option()) -> String {
return nodeChildrenToHtml(children: document.children, option)
}

public static func getVariantAliases(entry: [String: Any], contentTypeUid: String) throws -> [String: Any] {

try validateContentTypeUid(contentTypeUid)

guard let uid = entry["uid"] as? String, !uid.isEmpty else{
throw VariantUtilityError.invalidArgument("entry uid is required.")
}

guard let publish = entry["publish_details"] as? [String: Any],
let variants = publish["variants"] as? [String: Any] else{
return [
"entry_uid": uid,
"contenttype_uid": contentTypeUid,
"variants": [] as [String]
]
}
var aliases : [String] = []
for(_, value) in variants {
if let obj = value as? [String: Any],
let alias = obj["alias"] as? String {
aliases.append(alias)
}
}

return [
"entry_uid": uid,
"contenttype_uid": contentTypeUid,
"variants": aliases
]
}

public static func getVariantAliases(entries: [[String: Any]], contentTypeUid: String) throws -> [[String: Any]] {
try validateContentTypeUid(contentTypeUid)
return try entries.map { entry in
try getVariantAliases(entry: entry, contentTypeUid: contentTypeUid)
}
}

public static func getDataCsvariantsAttribute(entry: [String: Any]?, contentTypeUid: String) throws -> [String: Any]{
guard let e = entry else {
return ["data-csvariants": "[]"]
}

let payload = try getVariantAliases(entry: e, contentTypeUid: contentTypeUid)
let s = try jsonString(for: [payload])
return ["data-csvariants": s]

}

public static func getDataCsvariantsAttribute(entries: [[String: Any]], contentTypeUid: String) throws -> [String: Any]{
try validateContentTypeUid(contentTypeUid)
let payloads = try getVariantAliases(entries: entries, contentTypeUid: contentTypeUid)
let s = try jsonString(for: payloads)
return ["data-csvariants": s]
}


private static func validateContentTypeUid(_ contentTypeUid: String) throws {
if contentTypeUid.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
throw VariantUtilityError.invalidArgument("contentTypeUid must not be empty")
}
}

private static func jsonString(for array: [[String: Any]]) throws -> String{
let data = try JSONSerialization.data(withJSONObject: array, options: [])
guard let json = String(data: data, encoding: .utf8) else {
throw VariantUtilityError.invalidArgument("Failed to encode JSON string")
}
return json
}



static private func nodeChildrenToHtml(children nodes:[Node], _ option: Option) -> String {
nodes.map{ (node) -> String in
Expand Down Expand Up @@ -156,3 +233,4 @@ public struct ContentstackUtils {
return nil
}
}

21 changes: 21 additions & 0 deletions Tests/ContentstackUtilsTests/TestClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,25 @@ class TestDecodable {
static func getMultilevelEmbed() -> ContentBlock? {
return decode("EntryEmbedded")
}

/// Loads a JSON object from a file (e.g. `variantsEntries` → `variantsEntries.json`).
/// Tries the test bundle first (Xcode), then the directory containing this source file (SwiftPM `swift test`).
static func loadJSONObject(named fileName: String) throws -> [String: Any] {
let url: URL
if let path = Bundle(for: TestDecodable.self).path(forResource: fileName, ofType: "json") {
url = URL(fileURLWithPath: path)
} else {
let testsDir = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
let fallback = testsDir.appendingPathComponent("\(fileName).json")
guard FileManager.default.fileExists(atPath: fallback.path) else {
throw NSError(domain: "TestDecodable", code: 1, userInfo: [NSLocalizedDescriptionKey: "Missing json: \(fileName).json"])
}
url = fallback
}
let data = try Data(contentsOf: url, options: .mappedIfSafe)
guard let root = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
throw NSError(domain: "TestDecodable", code: 2, userInfo: [NSLocalizedDescriptionKey: "Root is not a JSON object"])
}
return root
}
}
Loading