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
122 changes: 121 additions & 1 deletion src/cslp/__test__/cslpdata.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,124 @@
import { extractDetailsFromCslp } from "../cslpdata";
import { extractDetailsFromCslp, isValidCslp } from "../cslpdata";

describe("isValidCslp", () => {
describe("valid cases", () => {
test("should return true for valid v1 format with 3 parts", () => {
expect(
isValidCslp("content_type_uid.entry_uid.locale")
).toBeTruthy();
});

test("should return true for valid v1 format with field path", () => {
expect(
isValidCslp("content_type_uid.entry_uid.locale.field_path")
).toBeTruthy();
});

test("should return true for valid v2 format with 3 parts", () => {
expect(
isValidCslp("v2:content_type_uid.entry_uid_variant_uid.locale")
).toBeTruthy();
});

test("should return true for valid v2 format with field path", () => {
expect(
isValidCslp(
"v2:content_type_uid.entry_uid_variant_uid.locale.field_path"
)
).toBeTruthy();
});
});

describe("invalid cases", () => {
test("should return false for null", () => {
expect(isValidCslp(null)).toBeFalsy();
});

test("should return false for undefined", () => {
expect(isValidCslp(undefined)).toBeFalsy();
});

test("should return false for empty string", () => {
expect(isValidCslp("")).toBeFalsy();
});

test("should return false for string with less than 3 parts", () => {
expect(isValidCslp("invalid")).toBeFalsy();
});

test("should return false for string with only 2 parts", () => {
expect(isValidCslp("a.b")).toBeFalsy();
});

test("should return false for v2 prefix with no data", () => {
expect(isValidCslp("v2:")).toBeFalsy();
});

test("should return false for v2 prefix with only 2 parts", () => {
expect(isValidCslp("v2:a.b")).toBeFalsy();
});

test("should return false for v2 format where entry_uid_variant_uid has no underscore", () => {
expect(
isValidCslp("v2:content_type_uid.entry.locale")
).toBeFalsy();
});

test("should return false for v2 format where entry_uid_variant_uid is missing variant_uid", () => {
expect(
isValidCslp("v2:content_type_uid.entry_.locale")
).toBeFalsy();
});

test("should return false for v2 format where entry_uid_variant_uid is missing entry_uid", () => {
expect(
isValidCslp("v2:content_type_uid._variant_uid.locale")
).toBeFalsy();
});

test("should return false for v2 format with empty parts", () => {
expect(isValidCslp("v2:..locale")).toBeFalsy();
});

test("should return false for v2 format with empty content_type_uid", () => {
expect(isValidCslp("v2:.entry_uid_variant_uid.locale")).toBeFalsy();
});

test("should return false for v2 format with empty locale", () => {
expect(
isValidCslp("v2:content_type_uid.entry_uid_variant_uid.")
).toBeFalsy();
});

test("should return false for v1 format with empty parts", () => {
expect(isValidCslp("..locale")).toBeFalsy();
});

test("should return false for v1 format with empty content_type_uid", () => {
expect(isValidCslp(".entry_uid.locale")).toBeFalsy();
});

test("should return false for v1 format with empty entry_uid", () => {
expect(isValidCslp("content_type_uid..locale")).toBeFalsy();
});

test("should return false for whitespace-only string", () => {
expect(isValidCslp(" ")).toBeFalsy();
});

test("should return false for tab and newline whitespace", () => {
expect(isValidCslp("\t\n")).toBeFalsy();
});

test("should return false for string with only dots", () => {
expect(isValidCslp("...")).toBeFalsy();
});

test("should return false for string with only two dots", () => {
expect(isValidCslp("..")).toBeFalsy();
});
});
});

describe("extractDetailsFromCslp", () => {
test("should extract details from a CSLP value string with nested multiple field", () => {
Expand Down
72 changes: 71 additions & 1 deletion src/cslp/cslpdata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,76 @@ import Config from "../configManager/configManager";
import { DeepSignal } from "deepsignal";
import { cslpTagStyles } from "../livePreview/editButton/editButton.style";

/**
* Validates that the required CSLP parts (content_type_uid, entry_uid/entry_uid_variant_uid, locale) are non-empty.
* @param parts The array of parts from splitting the CSLP string by "."
* @returns `true` if all required parts (first 3) are non-empty, `false` otherwise.
*/
function areRequiredPartsNonEmpty(parts: string[]): boolean {
// Check that we have at least 3 parts
if (parts.length < 3) {
return false;
}
// Verify that content_type_uid (parts[0]), entry_uid/entry_uid_variant_uid (parts[1]), and locale (parts[2]) are all non-empty
return parts[0].length > 0 && parts[1].length > 0 && parts[2].length > 0;
}

/**
* Validates if a CSLP value string is valid.
*
* Supports two formats:
* - **v1 format**: `content_type_uid.entry_uid.locale[.field_path]` (requires at least 3 parts)
* - **v2 format**: `v2:content_type_uid.entry_uid_variant_uid.locale[.field_path]`
* (requires at least 3 parts, entry_uid_variant_uid must contain underscore separating entry_uid and variant_uid)
*
* @param cslpValue The CSLP value string to validate (can be null or undefined).
* @returns Type predicate: `true` if the CSLP value is valid (narrows type to `string`), `false` otherwise.
*
* @example
* Valid v1 format
* isValidCslp("page.entry123.en-us") -> true
* isValidCslp("page.entry123.en-us.title") -> true
*
* Valid v2 format
* isValidCslp("v2:page.entry123_variant456.en-us") -> true
* isValidCslp("v2:page.entry123_variant456.en-us.title") -> true
*
* Invalid cases
* isValidCslp(null) -> false
* isValidCslp("invalid") -> false (less than 3 parts)
* isValidCslp("v2:page.entry123.en-us") -> false (missing underscore in entry_uid_variant_uid)
*/
export function isValidCslp(
cslpValue: string | null | undefined
): cslpValue is string {
// Return false for null, undefined, or empty string
if (!cslpValue) {
return false;
}

// Check for v2 format (starts with "v2:")
if (cslpValue.startsWith("v2:")) {
const dataAfterPrefix = cslpValue.substring(3); // Remove "v2:" prefix
const parts = dataAfterPrefix.split(".");
// v2 format requires at least 3 parts: content_type_uid.entry_uid_variant_uid.locale
// Verify that content_type_uid, entry_uid_variant_uid, and locale are all non-empty
if (!areRequiredPartsNonEmpty(parts)) {
return false;
}
// Verify that entry_uid_variant_uid (second part) contains both entry_uid and variant_uid separated by at least one underscore
const entryUidVariantUid = parts[1];
const entryVariantParts = entryUidVariantUid.split("_");
// Check that we have at least 2 parts (entry_uid and variant_uid) and all parts are non-empty
return entryVariantParts.length >= 2 && entryVariantParts.every((part) => part.length > 0);
}

// v1 format (default, no prefix)
const parts = cslpValue.split(".");
// v1 format requires at least 3 parts: content_type_uid.entry_uid.locale
// Verify that content_type_uid, entry_uid, and locale are all non-empty
return areRequiredPartsNonEmpty(parts);
}

/**
* Extracts details from a CSLP value string.
* @param cslpValue The CSLP value string to extract details from.
Expand Down Expand Up @@ -163,7 +233,7 @@ export function addCslpOutline(

const cslpTag = element.getAttribute("data-cslp");

if (trigger && cslpTag) {
if (trigger && isValidCslp(cslpTag)) {
if (elements.highlightedElement)
elements.highlightedElement.classList.remove(
cslpTagStyles()["cslp-edit-mode"]
Expand Down
4 changes: 2 additions & 2 deletions src/livePreview/editButton/editButton.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { effect } from "@preact/signals";
import { inIframe, isOpeningInNewTab } from "../../common/inIframe";
import Config from "../../configManager/configManager";
import { addCslpOutline, extractDetailsFromCslp } from "../../cslp";
import { addCslpOutline, extractDetailsFromCslp, isValidCslp } from "../../cslp";
import { cslpTagStyles } from "./editButton.style";
import { PublicLogger } from "../../logger/logger";
import {
Expand Down Expand Up @@ -439,7 +439,7 @@ export class LivePreviewEditButton {

const cslpTag = this.tooltip.getAttribute("current-data-cslp");

if (cslpTag) {
if (isValidCslp(cslpTag)) {
const {
content_type_uid,
entry_uid,
Expand Down
8 changes: 5 additions & 3 deletions src/timeline/compare/compare.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import timelinePostMessage from "../timelinePostMessage/timelinePostMessage";
import { timelinePostMessageEvents } from "../timelinePostMessage/timelinePostMessage.constant";
import { compareGlobalStyles } from "./compare.style";
import { isValidCslp } from "../../cslp/cslpdata";

const voidElements = new Set([
"area",
Expand Down Expand Up @@ -64,7 +65,8 @@ export function handleWebCompare() {
);
const map: Record<string, string> = {};
for (const element of elements) {
const cslp = element.getAttribute("data-cslp")!;
const cslp = element.getAttribute("data-cslp");
if (!isValidCslp(cslp)) continue;
if (
element.hasAttributes() &&
voidElements.has(element.tagName.toLowerCase())
Expand Down Expand Up @@ -101,8 +103,8 @@ export function handleWebCompare() {
document.querySelectorAll(LEAF_CSLP_SELECTOR)
);
for (const element of elements) {
const path = element.getAttribute("data-cslp")!;
if (!diff[path]) continue;
const path = element.getAttribute("data-cslp");
if (!isValidCslp(path) || !diff[path]) continue;

if (voidElements.has(element.tagName.toLowerCase())) {
element.classList.add(`cs-compare__void--${operation}`);
Expand Down
4 changes: 2 additions & 2 deletions src/visualBuilder/components/fieldLabelWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import classNames from "classnames";
import React, { useEffect, useState } from "preact/compat";
import { extractDetailsFromCslp } from "../../cslp";
import { extractDetailsFromCslp, isValidCslp } from "../../cslp";
import { CslpData } from "../../cslp/types/cslp.types";
import { VisualBuilderCslpEventDetails } from "../types/visualBuilder.types";
import { FieldSchemaMap } from "../utils/fieldSchemaMap";
Expand Down Expand Up @@ -144,7 +144,7 @@ function FieldLabelWrapperComponent(
const domAncestor = eventDetails.editableElement.closest(`[data-cslp]:not([data-cslp^="${props.fieldMetadata.content_type_uid}"])`);
if(domAncestor) {
const domAncestorCslp = domAncestor.getAttribute("data-cslp");
if (domAncestorCslp) {
if (isValidCslp(domAncestorCslp)) {
const domAncestorDetails = extractDetailsFromCslp(domAncestorCslp);
const domAncestorContentTypeUid = domAncestorDetails.content_type_uid;
const domAncestorContentParent = referenceData?.find(data => data.contentTypeUid === domAncestorContentTypeUid);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
import { VisualBuilder } from "../..";
import { FieldSchemaMap } from "../../utils/fieldSchemaMap";
import { handleRevalidateFieldData } from "../useRevalidateFieldDataPostMessageEvent";

import * as cslpdata from "../../../cslp";
// Mock dependencies
vi.mock("../../utils/fieldSchemaMap", () => ({
FieldSchemaMap: {
Expand All @@ -11,9 +11,7 @@ vi.mock("../../utils/fieldSchemaMap", () => ({
},
}));

vi.mock("../../../cslp", () => ({
extractDetailsFromCslp: vi.fn(),
}));
vi.spyOn(cslpdata, "extractDetailsFromCslp");

vi.mock("../../generators/generateOverlay", () => ({
hideFocusOverlay: vi.fn(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ describe("useVariantFieldsPostMessageEvent", () => {

// Reset mocks
vi.clearAllMocks();

// Mock isValidCslp to return true for test data (after clearAllMocks)
vi.spyOn(cslpdata, "isValidCslp").mockReturnValue(true);
});

afterEach(() => {
Expand Down Expand Up @@ -362,6 +365,9 @@ describe("addVariantFieldClass", () => {

// Reset mocks
vi.clearAllMocks();

// Mock isValidCslp to return true for test data
vi.spyOn(cslpdata, "isValidCslp").mockReturnValue(true);
});

afterEach(() => {
Expand Down Expand Up @@ -423,6 +429,7 @@ describe("addVariantFieldClass", () => {
variant: cslpValue.split(":")[1]
}
});

const variantUid = "variant-456";
const variantOrder = ["variant-123", "variant-456"];
VisualBuilder.VisualBuilderGlobalState.value.variantOrder = variantOrder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import livePreviewPostMessage from "../../livePreview/eventManager/livePreviewEv
import { LIVE_PREVIEW_POST_MESSAGE_EVENTS } from "../../livePreview/eventManager/livePreviewEventManager.constant";
import { DATA_CSLP_ATTR_SELECTOR } from "../utils/constants";
import { visualBuilderStyles } from "../visualBuilder.style";
import { isValidCslp } from "../../cslp/cslpdata";
import { setHighlightVariantFields } from "./useVariantsPostMessageEvent";

const VARIANT_UPDATE_DELAY_MS: Readonly<number> = 8000;
Expand Down Expand Up @@ -37,7 +38,7 @@ export function updateVariantClasses(): void {
dataCslp: string,
observer?: MutationObserver
) => {
if (!dataCslp) return;
if (!isValidCslp(dataCslp)) return;

if (
dataCslp.startsWith("v2:") &&
Expand Down Expand Up @@ -84,7 +85,7 @@ export function updateVariantClasses(): void {
const addElementClasses = (element: HTMLElement) => {
const dataCslp = element.getAttribute(DATA_CSLP_ATTR_SELECTOR);

if (!dataCslp) {
if (!isValidCslp(dataCslp)) {
//recursive call for child nodes
element.childNodes.forEach((child) => {
if (child instanceof HTMLElement) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { VisualBuilder } from "..";
import { extractDetailsFromCslp } from "../../cslp";
import { extractDetailsFromCslp, isValidCslp } from "../../cslp";
import { FieldSchemaMap } from "../utils/fieldSchemaMap";
import { hideFocusOverlay } from "../generators/generateOverlay";
import { handleBuilderInteraction } from "../listeners/mouseClick";
Expand Down Expand Up @@ -32,7 +32,7 @@ export async function handleRevalidateFieldData(): Promise<void> {

if (targetElement) {
const cslp = targetElement.getAttribute("data-cslp");
if (cslp) {
if (isValidCslp(cslp)) {
const fieldMetadata = extractDetailsFromCslp(cslp);

// Try to revalidate specific field schema and data
Expand All @@ -51,7 +51,7 @@ export async function handleRevalidateFieldData(): Promise<void> {
window.location.reload();
} finally {
// Step 3: Refocus the element if we had one focused before
if (shouldRefocus && elementCslp) {
if (shouldRefocus && isValidCslp(elementCslp)) {
await refocusElement(elementCslp, elementCslpUniqueId);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types
import { FieldSchemaMap } from "../utils/fieldSchemaMap";
import { updateVariantClasses } from "./useRecalculateVariantDataCSLPValues";
import { debounce } from "lodash-es";
import { extractDetailsFromCslp } from "../../cslp/cslpdata";
import { extractDetailsFromCslp, isValidCslp } from "../../cslp/cslpdata";

interface VariantFieldsEvent {
data: {
Expand Down Expand Up @@ -61,7 +61,7 @@ export function addVariantFieldClass(
const elements = document.querySelectorAll(`[data-cslp]`);
elements.forEach((element) => {
const dataCslp = element.getAttribute("data-cslp");
if (!dataCslp) return;
if (!isValidCslp(dataCslp)) return;

if (dataCslp?.includes(variant_uid)) {
element.classList.add("visual-builder__variant-field");
Expand Down
Loading
Loading