decap-cms-core
Version:
Decap CMS core application, see decap-cms package for the main distribution.
146 lines (132 loc) • 4.54 kB
text/typescript
import { vercelStegaEncode } from '@vercel/stega';
import { isImmutableMap, isImmutableList } from '../types/immutable';
import type { Map as ImmutableMap, List } from 'immutable';
import type { CmsField } from 'decap-cms-core';
/**
* Context passed to encode functions, containing the current state of the encoding process
*/
interface EncodeContext {
fields: CmsField[]; // Available CMS fields at current level
path: string; // Path to current value in object tree
visit: (value: unknown, fields: CmsField[], path: string) => unknown; // Visitor for recursive traversal
}
/**
* Get the fields that should be used for encoding nested values
*/
function getNestedFields(f?: CmsField): CmsField[] {
if (f) {
if ('types' in f) {
return f.types ?? [];
}
if ('fields' in f) {
return f.fields ?? [];
}
if ('field' in f) {
return f.field ? [f.field] : [];
}
return [f];
}
return [];
}
/**
* Encode a string value by appending steganographic data
* For markdown fields, encode each paragraph separately
*/
function encodeString(value: string, { fields, path }: EncodeContext): string {
const [field] = fields;
if (!field) return value;
const { widget } = field;
if (widget === 'string' || widget === 'text') {
if ('visualEditing' in field && field.visualEditing === false) return value;
const stega = vercelStegaEncode({ decap: path });
return value + stega;
}
if (widget === 'markdown') {
const stega = vercelStegaEncode({ decap: path });
const blocks = value.split(/(\n\n+)/);
return blocks.map(block => (block.trim() ? block + stega : block)).join('');
}
return value;
}
/**
* Encode a list of values, handling both simple values and nested objects/lists
* For typed lists, use the type field to determine which fields to use
*/
function encodeList(list: List<unknown>, ctx: EncodeContext): List<unknown> {
let newList = list;
for (let i = 0; i < newList.size; i++) {
const item = newList.get(i);
if (isImmutableMap(item)) {
const itemType = item.get('type');
if (typeof itemType === 'string') {
// For typed items, look up fields based on type
const field = ctx.fields.find(f => f.name === itemType);
const newItem = ctx.visit(item, getNestedFields(field), `${ctx.path}.${i}`);
newList = newList.set(i, newItem);
} else {
// For untyped items, use current fields
const newItem = ctx.visit(item, ctx.fields, `${ctx.path}.${i}`);
newList = newList.set(i, newItem);
}
} else {
// For simple values, use first field if available
const field = ctx.fields[0];
const newItem = ctx.visit(item, field ? [field] : [], `${ctx.path}.${i}`);
if (newItem !== item) {
newList = newList.set(i, newItem);
}
}
}
return newList;
}
/**
* Encode a map of values, looking up the appropriate field for each key
* and recursively encoding nested values
*/
function encodeMap(
map: ImmutableMap<string, unknown>,
ctx: EncodeContext,
): ImmutableMap<string, unknown> {
let newMap = map;
for (const [key, val] of newMap.entrySeq().toArray()) {
const field = ctx.fields.find(f => f.name === key);
if (field) {
const fields = getNestedFields(field);
const newVal = ctx.visit(val, fields, ctx.path ? `${ctx.path}.${key}` : key);
if (newVal !== val) {
newMap = newMap.set(key, newVal);
}
}
}
return newMap;
}
/**
* Cache for encoded values to prevent re-encoding unchanged values
* across keystrokes. The cache is keyed by path.
*/
const encodingCache = new Map();
/**
* Main entry point for encoding steganographic data into entry values
* Uses a visitor pattern with caching to handle recursive structures
*/
export function encodeEntry(value: unknown, fields: List<ImmutableMap<string, unknown>>) {
const plainFields = fields.toJS() as CmsField[];
function visit(value: unknown, fields: CmsField[], path = '') {
const cached = encodingCache.get(path);
if (cached === value) return value;
const ctx: EncodeContext = { fields, path, visit };
let result;
if (isImmutableList(value)) {
result = encodeList(value, ctx);
} else if (isImmutableMap(value)) {
result = encodeMap(value, ctx);
} else if (typeof value === 'string') {
result = encodeString(value, ctx);
} else {
result = value;
}
encodingCache.set(path, result);
return result;
}
return visit(value, plainFields);
}