UNPKG

decap-cms-core

Version:

Decap CMS core application, see decap-cms package for the main distribution.

146 lines (132 loc) 4.54 kB
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); }