<commit_before>/*global getResource importModule parseSkin */

require('core/string');
require('core/object');
import('helma/filters', 'filters');
import('helma/logging', 'logging');
import('helma/system', 'system');

export('render', 'createSkin', 'Skin');

var __shared__ = true;
var log = logging.getLogger(__name__);
var skincache = false; // {}

system.addHostObject(org.helma.template.MacroTag);

/**
 * Parse a skin from a resource and render it using the given context.
 * @param skinOrResource a skin object, helma resource, or file name
 * @param context the skin render context
 * @param scope optional scope object for relative resource lookup
 */
function render(skinOrResource, context, scope) {
    scope = scope || this;
    var skin;
    if (typeof(skinOrResource.render) == "function") {
        skin = skinOrResource;
    } else if (skinOrResource instanceof org.helma.repository.Resource) {
        skin = createSkin(skinOrResource, scope);
    } else if (typeof(skinOrResource) == "string") {
        var subskin;
        if (skinOrResource.indexOf('#') > -1) {
            [skinOrResource, subskin] = skinOrResource.split('#');
        }
        var resource = scope.getResource(skinOrResource);
        skin = createSkin(resource, scope);
        if (subskin) {
            skin = skin.getSubskin(subskin);
        }
    } else {
        throw Error("Unknown skin object: " + skinOrResource);
    }
    return skin.render(context);
}

/**
 * Parse a skin from a resource.
 * @param resource
 */
function createSkin(resourceOrString, scope) {
    if (this.skincache && resourceOrString in skincache) {
        return skincache[resourceOrString];
    }
    if (log.isDebugEnabled())
        log.debug("creating skin: " + resourceOrString);
    var mainSkin = [];
    var subSkins = {};
    var currentSkin = mainSkin;
    var parentSkin = null;
    parseSkin(resourceOrString, function(part) {
        if (part.name === 'extends') {
            var skinPath = part.getParameter(0);
            var skinResource;
            if (resourceOrString.parentRepository) {
                skinResource = resourceOrString.parentRepository.getResource(skinPath);
            }
            if (!skinResource || !skinResource.exists()) {
                skinResource = scope.getResource(skinPath);
            }
            parentSkin = createSkin(skinResource);
        } else if (part.name === 'subskin')  {
            var skinName = part.getParameter('name', 0);
            currentSkin = [];
            subSkins[skinName] = currentSkin;
        } else {
            currentSkin[currentSkin.length] = part;
        }
    });
    // normalization: cut trailing whitespace so it's
    // easier to tell if main skin shoule be inherited
    var lastPart = mainSkin[mainSkin.length - 1];
    if (typeof(lastPart) === 'string' && lastPart.trim() === '') {
        mainSkin.pop();
    }
    var skin = new Skin(mainSkin, subSkins, parentSkin);
    if (skincache)
        skincache[resourceOrString] = skin;
    return skin;
}

/**
 * The Skin object. This takes an array of skin parts (literal strings and MacroTags)
 * and a dictionary of subskins.
 * @param mainSkin an array of skin parts: string literals and macro tags
 * @param subSkins a dictionary of named skin components
 */
function Skin(mainSkin, subSkins, parentSkin) {

    var self = this;

    this.render = function render(context) {
        if (mainSkin.length === 0 && parentSkin) {
            return renderInternal(parentSkin.getSkinParts(), context);
        } else {
            return renderInternal(mainSkin, context);
        }
    };

    this.renderSubskin = function renderSubskin(skinName, context) {
        if (!subSkins[skinName] && parentSkin) {
            return renderInternal(parentSkin.getSkinParts(skinName), context);
        } else {
            return renderInternal(subSkins[skinName], context);
        }
    };

    this.getSubskin = function getSubskin(skinName) {
        if (subSkins[skinName]) {
            return new Skin(subSkins[skinName], subSkins, parentSkin);
        } else {
            return null;
        }
    };

    this.getSkinParts = function getSkinParts(skinName) {
        var parts = skinName ? subSkins[skinName] : mainSkin;
        if (!parts || (!skinName && parts.length === 0)) {
            return parentSkin ? parentSkin.getSkinParts(skinName) : null;
        }
        return parts;
    };

    function renderInternal(parts, context) {
        // extend context by globally provided filters. user-provided filters
        // override globally defined ones
        context = Object.merge(context, filters);
        return [renderPart(part, context) for each (part in parts)].join('');
    }

    function renderPart(part, context) {
        return part instanceof MacroTag && part.name ?
                evaluateMacro(part, context) : part;
    }

    function evaluateMacro(macro, context) {
        // evaluate the macro itself
        var value = evaluateExpression(macro, context, '_macro');
        if (value instanceof Array) {
            value = value.join('');
        }
        // traverse the linked list of filters
        for (var filter = macro.filter; filter; filter = filter.filter) {
            // make sure value is not undefined, otherwise evaluateExpression()
            // might become confused
            if (!isVisible(value)) {
                value = "";
            }
            value = evaluateExpression(filter, context, '_filter', value);
        }
        return value
    }

    function evaluateExpression(macro, context, suffix, value) {
        if (log.isDebugEnabled())
            log.debug('evaluating expression: ' + macro);
        if (builtin[macro.name]) {
            return builtin[macro.name](macro, context);
        }
        var path = macro.name.split('.');
        var elem = context;
        var length = path.length;
        var last = path[length-1];
        for (var i = 0; i < length - 1; i++) {
            elem = elem[path[i]];
            if (!isDefined(elem)) {
                break;
            }<commit_msg>Two new features in helma/skin: Read macro/filter modules from macros array in  webapp config, and implement filter chains for subskins.<commit_after>/*global getResource importModule parseSkin */

require('core/string');
require('core/object');
import('helma/logging', 'logging');

export('render', 'createSkin', 'Skin');

var __shared__ = true;
var log = logging.getLogger(__name__);
var skincache = false; // {}

require('helma/system').addHostObject(org.helma.template.MacroTag);

/**
 * Parse a skin from a resource and render it using the given context.
 * @param skinOrResource a skin object, helma resource, or file name
 * @param context the skin render context
 * @param scope optional scope object for relative resource lookup
 */
function render(skinOrResource, context, scope) {
    scope = scope || this;
    var skin;
    if (typeof(skinOrResource.render) == "function") {
        skin = skinOrResource;
    } else if (skinOrResource instanceof org.helma.repository.Resource) {
        skin = createSkin(skinOrResource, scope);
    } else if (typeof(skinOrResource) == "string") {
        var subskin;
        if (skinOrResource.indexOf('#') > -1) {
            [skinOrResource, subskin] = skinOrResource.split('#');
        }
        var resource = scope.getResource(skinOrResource);
        skin = createSkin(resource, scope);
        if (subskin) {
            skin = skin.getSubskin(subskin);
        }
    } else {
        throw Error("Unknown skin object: " + skinOrResource);
    }
    // extend context by globally provided macros and filters.
    // user-provided context overrides globally defined stuff
    var config = require('helma/webapp/env').config;
    if (config && config.macros instanceof Array) {
        for each (var module in config.macros) {
            context = Object.merge(context, require(module));
        }
    }
    return skin.render(context);
}

/**
 * Parse a skin from a resource.
 * @param resource
 */
function createSkin(resourceOrString, scope) {
    if (this.skincache && resourceOrString in skincache) {
        return skincache[resourceOrString];
    }
    if (log.isDebugEnabled())
        log.debug("creating skin: " + resourceOrString);
    var mainSkin = [];
    var subSkins = {};
    var currentSkin = mainSkin;
    var parentSkin = null;
    parseSkin(resourceOrString, function(part) {
        if (part.name === 'extends') {
            var skinPath = part.getParameter(0);
            var skinResource;
            if (resourceOrString.parentRepository) {
                skinResource = resourceOrString.parentRepository.getResource(skinPath);
            }
            if (!skinResource || !skinResource.exists()) {
                skinResource = scope.getResource(skinPath);
            }
            parentSkin = createSkin(skinResource);
        } else if (part.name === 'subskin')  {
            var skinName = part.getParameter('name', 0);
            currentSkin = [];
            currentSkin.filter = part.filter;
            subSkins[skinName] = currentSkin;
        } else {
            currentSkin[currentSkin.length] = part;
        }
    });
    // normalization: cut trailing whitespace so it's
    // easier to tell if main skin shoule be inherited
    var lastPart = mainSkin[mainSkin.length - 1];
    if (typeof(lastPart) === 'string' && lastPart.trim() === '') {
        mainSkin.pop();
    }
    var skin = new Skin(mainSkin, subSkins, parentSkin);
    if (skincache)
        skincache[resourceOrString] = skin;
    return skin;
}

/**
 * The Skin object. This takes an array of skin parts (literal strings and MacroTags)
 * and a dictionary of subskins.
 * @param mainSkin an array of skin parts: string literals and macro tags
 * @param subSkins a dictionary of named skin components
 */
function Skin(mainSkin, subSkins, parentSkin) {

    var self = this;

    this.render = function render(context) {
        if (mainSkin.length === 0 && parentSkin) {
            return renderInternal(parentSkin.getSkinParts(), context);
        } else {
            return renderInternal(mainSkin, context);
        }
    };

    this.renderSubskin = function renderSubskin(skinName, context) {
        if (!subSkins[skinName] && parentSkin) {
            return renderInternal(parentSkin.getSkinParts(skinName), context);
        } else {
            return renderInternal(subSkins[skinName], context);
        }
    };

    this.getSubskin = function getSubskin(skinName) {
        if (subSkins[skinName]) {
            return new Skin(subSkins[skinName], subSkins, parentSkin);
        } else {
            return null;
        }
    };

    this.getSkinParts = function getSkinParts(skinName) {
        var parts = skinName ? subSkins[skinName] : mainSkin;
        if (!parts || (!skinName && parts.length === 0)) {
            return parentSkin ? parentSkin.getSkinParts(skinName) : null;
        }
        return parts;
    };

    function renderInternal(parts, context) {
        var value = [renderPart(part, context) for each (part in parts)].join('');
        if (parts && parts.filter)
            return evaluateFilter(value, parts.filter, context);
        return value;
    }

    function renderPart(part, context) {
        return part instanceof MacroTag && part.name ?
                evaluateMacro(part, context) : part;
    }

    function evaluateMacro(macro, context) {
        // evaluate the macro itself
        var value = evaluateExpression(macro, context, '_macro');
        if (value instanceof Array) {
            value = value.join('');
        }
        return evaluateFilter(value, macro.filter, context);
    }

    function evaluateFilter(value, filter, context) {
        // traverse the linked list of filters
        while (filter) {
            // make sure value is not undefined, otherwise evaluateExpression()
            // might become confused
            if (!isVisible(value)) {
                value = "";
            }
            value = evaluateExpression(filter, context, '_filter', value);
            filter = filter.filter;
        }
        return value;
    }

    function evaluateExpression(macro, context, suffix, value) {
        if (log.isDebugEnabled())
            log.debug('evaluating expression: ' + macro);
        if (builtin[macro.name]) {
            return builtin[macro.name](macro, context);
        }
        var path = macro.name.split('.');
        var elem = context;
        var length = path.length;
        var last = path[length-1];
        for (var i = 0; i < length - 1; i++) {
            elem = elem[path[i]];
            if (!isDefined(elem)) {
                break;
            }
{'fraction_non_alphanumeric': 0.11459476406960338, 'fraction_numerical': 0.0014108794481893713, 'mean_word_length': 2.4060331019754404, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4784727', 'n_tokens_mistral': 3576, 'n_tokens_neox': 3496, 'n_words': 956}