@lionrockjs/mod-admin-cms
Version:
The CMS module for LionRockJS
399 lines (326 loc) • 15.6 kB
JavaScript
import {HelperPageText} from "@lionrockjs/mod-cms-read";
import fs from "node:fs";
import {Controller} from "@lionrockjs/mvc";
/**
* Private helper class containing merge utility methods
* @private
*/
class Merger {
/**
* Merges language-specific values from target and source objects
*/
static mergeLanguageValues(targetValues = {}, sourceValues = {}) {
const languageSet = new Set([...Object.keys(targetValues), ...Object.keys(sourceValues)]);
const result = {};
languageSet.forEach(language => {
const targetLangValues = targetValues[language] || {};
const sourceLangValues = sourceValues[language] || {};
result[language] = {...targetLangValues, ...sourceLangValues};
});
return result;
}
/**
* Merges basic properties (attributes and pointers) from target and source objects
*/
static mergeBasicProps(target = {}, source = {}) {
return {
attributes: {...(target.attributes || {}), ...(source.attributes || {})},
pointers: {...(target.pointers || {}), ...(source.pointers || {})}
};
}
/**
* Merges arrays of items, handling nested properties
*/
static mergeItemArrays(targetItems = [], sourceItems = []) {
const result = [];
const length = Math.max(targetItems.length, sourceItems.length);
for (let i = 0; i < length; i++) {
const targetItem = targetItems[i];
const sourceItem = sourceItems[i];
// If one side is missing, use the other
if (!targetItem && !sourceItem) {
result.push(null);
continue;
}
if (targetItem && !sourceItem) {
result.push(targetItem);
continue;
}
if (!targetItem && sourceItem) {
result.push(sourceItem);
continue;
}
// Merge the items
const mergedItem = {
...this.mergeBasicProps(targetItem, sourceItem),
values: this.mergeLanguageValues(targetItem.values, sourceItem.values)
};
// Handle nested items if they exist
if (targetItem.items || sourceItem.items) {
mergedItem.items = this.mergeItems(targetItem.items, sourceItem.items);
}
result.push(mergedItem);
}
return result.filter(item => !!item);
}
/**
* Merges item collections from target and source objects
*/
static mergeItems(targetItems = {}, sourceItems = {}) {
const result = {};
const itemTypes = new Set([...Object.keys(targetItems), ...Object.keys(sourceItems)]);
itemTypes.forEach(itemType => {
result[itemType] = this.mergeItemArrays(
targetItems[itemType] || [],
sourceItems[itemType] || []
);
});
return result;
}
}
export default class HelperPageEdit{
static getProps(rawKey, prefix=""){
const keyParts = rawKey.split(':');
return {
name: keyParts[0].replace(prefix,'').split('__')[0],
type: keyParts[1] || 'text',
};
}
static getPointerProps(rawKey){
const keyParts = rawKey.split(':');
//if keyParts[0] matches @ or ., default type to 'page/field', else page/basic
return {
name: keyParts[0].replace("*",'').split('__')[0],
type: keyParts[1] || ((/[.@]/.test(keyParts[0])) ? 'text': 'page/basic'),
};
}
static get_blueprint_props(config_blueprint){
//deep copy config
const blueprint = JSON.parse(JSON.stringify(config_blueprint))
const attributes = [];
const fields = [];
const items = [];
const pointers = [];
blueprint.forEach(it => {
if(typeof it === 'object'){
Object.keys(it).forEach(key => {
const rawAttributes = it[key].filter(it => typeof it === 'string' && /^@/.test(it));
const rawPointers = it[key].filter(it => typeof it === 'string' && /^\*/.test(it));
const rawFields = it[key].filter(it => typeof it === 'string' && /^[^@*]/.test(it));
const nestedItems = it[key].filter(it => typeof it === 'object');
const attributes = rawAttributes.map(it => this.getProps(it, '@'));
const pointers = rawPointers.map(it => this.getPointerProps(it));
const fields = rawFields.map(it => this.getProps(it));
const nestedItemsProps = [];
nestedItems.forEach(nestedItem => {
Object.keys(nestedItem).forEach(nestedKey => {
const nestedRawAttributes = nestedItem[nestedKey].filter(it => typeof it === 'string' && /^@/.test(it));
const nestedRawPointers = nestedItem[nestedKey].filter(it => typeof it === 'string' && /^\*/.test(it));
const nestedRawFields = nestedItem[nestedKey].filter(it => typeof it === 'string' && /^[^@*]/.test(it));
nestedItemsProps.push({
name: nestedKey,
attributes: nestedRawAttributes.map(it => this.getProps(it, '@')),
pointers: nestedRawPointers.map(it => this.getPointerProps(it)),
fields: nestedRawFields.map(it => this.getProps(it)),
});
});
});
items.push({
name: key,
attributes: attributes,
pointers: pointers,
fields: fields,
items: nestedItemsProps.length > 0 ? nestedItemsProps : undefined,
});
});
}else if(/^@/.test(it)){
attributes.push(this.getProps(it, '@'));
}else if(/^\*/.test(it)){
pointers.push(this.getPointerProps(it));
}else{
fields.push(this.getProps(it));
}
});
return{
attributes,
pointers,
fields,
items
}
}
static definitionInstance(definitions=[]){
const result = {};
definitions.forEach(it => {result[it] = ""});
return result;
}
static blueprint(pageType, blueprints={}, defaultLanguage="en"){
const original = HelperPageText.defaultOriginal();
original.values[defaultLanguage] = {};
const blueprint = blueprints[pageType] ?? blueprints.default;
if(!blueprint)return original;
const attributes = blueprint.filter(it => typeof it !== 'object').filter(it => /^@/.test(it)).map(it => it.substring(1).split(":")[0]);
const pointers = blueprint.filter(it => typeof it !== 'object').filter(it => /^\*/.test(it)).map(it => it.substring(1).split(":")[0]);
const values = blueprint.filter(it => typeof it !== 'object').filter(it => /^[^@*]/.test(it)).map(it => it.split(":")[0]);
const items = blueprint.filter(it => typeof it === 'object')
original.attributes = {_type:pageType, ...this.definitionInstance(attributes)};
original.pointers = this.definitionInstance(pointers);
original.values[defaultLanguage] = this.definitionInstance(values);
items.forEach(item =>{
const key = Object.keys(item)[0];
const itemAttributes = item[key].filter(it=> typeof it === 'string' && /^@/.test(it)).map(it => it.substring(1).split(":")[0]);
const itemPointers = item[key].filter(it=> typeof it === 'string' && /^\*/.test(it)).map(it => it.substring(1).split(":")[0]);
const itemValues = item[key].filter(it => typeof it === 'string' && /^[^@*]/.test(it)).map(it => it.split(":")[0]);
const nestedItems = item[key].filter(it => typeof it === 'object');
const defaultItem = HelperPageText.defaultOriginalItem();
defaultItem.attributes._weight = 0;
Object.assign(defaultItem.attributes, this.definitionInstance(itemAttributes));
Object.assign(defaultItem.pointers, this.definitionInstance(itemPointers));
defaultItem.values[defaultLanguage] = this.definitionInstance(itemValues);
// Handle nested items recursively
nestedItems.forEach(nestedItem => {
const nestedKey = Object.keys(nestedItem)[0];
const nestedItemAttributes = nestedItem[nestedKey].filter(it=> typeof it === 'string' && /^@/.test(it)).map(it => it.substring(1).split(":")[0]);
const nestedItemPointers = nestedItem[nestedKey].filter(it=> typeof it === 'string' && /^\*/.test(it)).map(it => it.substring(1).split(":")[0]);
const nestedItemValues = nestedItem[nestedKey].filter(it => typeof it === 'string' && /^[^@*]/.test(it)).map(it => it.split(":")[0]);
const nestedDefaultItem = HelperPageText.defaultOriginalItem();
nestedDefaultItem.attributes._weight = 0;
Object.assign(nestedDefaultItem.attributes, this.definitionInstance(nestedItemAttributes));
Object.assign(nestedDefaultItem.pointers, this.definitionInstance(nestedItemPointers));
nestedDefaultItem.values[defaultLanguage] = this.definitionInstance(nestedItemValues);
defaultItem.items = defaultItem.items || {};
defaultItem.items[nestedKey] = [nestedDefaultItem];
});
original.items[key] = [defaultItem];
})
return original;
}
static postToOriginal($_POST, langauge="en"){
const original = HelperPageText.defaultOriginal();
original.values[langauge] = {};
const blockPosts = [];
Object.keys($_POST).sort().forEach(name => {
//parse attributes
const value = $_POST[name];
let m = name.match(/^@(\w+)$/);
if(m){
original.attributes[m[1]] = value;
return;
}
//parse pointers
m = name.match(/^\*(\w+)$/)
if(m){
const key = m[1];
original.pointers[key] = value;
return;
}
//parse values
m = name.match(/^\.(\w+)\|?([a-z-]+)?$/);
if(m){
original.values[ m[2] || langauge ] ||= {};
original.values[ m[2] || langauge ][ m[1] ] = value;
return;
}
//parse items (including nested items)
// First try nested items pattern: .items[0].nested_items[1]@attribute or .items[0].nested_items[1].field
m = name.match(/^\.(\w+)\[(\d+)\]\.(\w+)\[(\d+)\](@(\w+)$|\.(\w+)\|?([a-z-]+)?$|\*(\w+)$)/);
if(m){
original.items[ m[1] ] ||= [];
original.items[ m[1] ][ parseInt(m[2]) ] ||= HelperPageText.defaultOriginalItem();
const parentItem = original.items[ m[1] ][ parseInt(m[2]) ];
parentItem.items ||= {};
parentItem.items[ m[3] ] ||= [];
parentItem.items[ m[3] ][ parseInt(m[4]) ] ||= HelperPageText.defaultOriginalItem();
const nestedItem = parentItem.items[ m[3] ][ parseInt(m[4]) ];
if(m[6]){
nestedItem.attributes[ m[6] ] = value;
return;
}
if(m[7]){
nestedItem.values[ m[8] || langauge ] ||= {};
nestedItem.values[ m[8] || langauge ][ m[7] ] = value;
return;
}
if(m[9]){
nestedItem.pointers[ m[9] ] = value;
return;
}
}
// Then try regular items pattern: .items[0]@attribute or .items[0].field
m = name.match(/^\.(\w+)\[(\d+)\](@(\w+)$|\.(\w+)\|?([a-z-]+)?$|\*(\w+)$)/);
if(m){
original.items[ m[1] ] ||= [];
original.items[ m[1] ][ parseInt(m[2]) ] ||= HelperPageText.defaultOriginalItem();
const item = original.items[ m[1] ][ parseInt(m[2]) ];
if(m[4]){
item.attributes[ m[4] ] = value;
return;
}
if(m[5]){
item.values[ m[6] || langauge ] ||= {};
item.values[ m[6] || langauge ][ m[5] ] = value;
return;
}
if(m[7]){
item.pointers[ m[7] ] = value;
return;
}
}
//collect blocks
m = name.match(/^#(\d+)([.@*][\w+\[\].@*|-]+)$/);
if(m){
const key = parseInt(m[1]);
blockPosts[ key ] ||= {};
const post = blockPosts[ key ];
post[m[2]] = value;
}
});
//loop blockPosts and merge them into original.blocks
original.blocks ||= [];
blockPosts.forEach((post, index) => {
original.blocks[index] = this.mergeOriginals(
original.blocks[index] || HelperPageText.defaultOriginal(),
this.postToOriginal(post, langauge)
);
delete original.blocks[index].blocks; //blocks should not be nested
})
return original;
}
/**
* Merges two original objects by combining their properties
*/
static mergeOriginals(target, source) {
// Create default result structure
const result = HelperPageText.defaultOriginal();
// Merge basic properties
const basicProps = Merger.mergeBasicProps(target, source);
result.attributes = basicProps.attributes;
result.pointers = basicProps.pointers;
// Merge language values
result.values = Merger.mergeLanguageValues(target.values, source.values);
// Merge items
result.items = Merger.mergeItems(target.items, source.items);
// Merge blocks if they exist
if (target.blocks || source.blocks) {
const targetBlocks = target.blocks || [];
const sourceBlocks = source.blocks || [];
// Use the same item merging logic for blocks
result.blocks = Merger.mergeItemArrays(targetBlocks, sourceBlocks);
}
return result;
}
static getOriginal(page, attributes={}, state=new Map()){
const version = state.get(Controller.STATE_QUERY)?.version;
if(version){
const versionFile = `${Central.config.cms.versionPath}/${page.id}/${version}.json`;
if(fs.existsSync(versionFile)){
return JSON.parse(fs.readFileSync(versionFile));
}else{
throw new Error(`Version ${version} not found`);
}
}
if(!page.original)return HelperPageText.defaultOriginal();
const original = JSON.parse(page.original);
Object.assign(original.attributes, attributes);
return original;
}
}