src/Attributes.js

const _attributes = Symbol.for('attributes');
const Node = require('./Node');
const bplist = require('bplist-parser');

/**
 * Represents a dictionary of attributes
 *
 * @extends {Node}
 *
 * @property {object|undefined} attributes - Object containing the parsed attributes (they are stored as BASE64 encoded Binary Plists)
 *
 * @example
 * //Get an MSAttributedString instance
 * const node = sketch.pages[0].get('MSAttributedString');
 *
 * //Parse and return the attributes
 * console.log(node.attributes)
 */
class Attributes extends Node {
    get attributes() {
        if (this[_attributes]) {
            return this[_attributes];
        }

        const attributes = parseArchive(this.archivedAttributedString._archive);

        this[_attributes] = attributes;
        return attributes;
    }
}

module.exports = Attributes;

function parseArchive(stringArchive) {
    const archive = bplist.parseBuffer(Buffer.from(stringArchive, 'base64'));
    const result = {};
    const objects = archive[0].$objects;
    const root = archive[0].$top.root.UID;
    const rootObject = objects[root];

    for (var key in rootObject) {
        if (rootObject[key].UID) {
            result[key] = getReferenceById(rootObject[key].UID);
        }
    }

    return result;

    function getReferenceById(id) {
        const object = objects[id];

        if (typeof object === 'string' || typeof object === 'number' || typeof object === 'boolean') {
            return object;
        }

        const reference = {};

        if (typeof object === 'object') {
            for (let index in object) {
                if (object[index].UID) {
                    reference[index] = getReferenceById(object[index].UID);
                } else if (Array.isArray(object[index]) && index !== 'NS.keys' && index !== 'NS.objects') {
                    reference[index] = [];
                    object[index].forEach(ao => {
                        if (ao.UID) {
                            reference[index].push(getReferenceById(ao.UID));
                        } else {
                            reference[index].push(ao);
                        }
                    });
                } else if (index !== 'NS.keys' && index !== 'NS.objects') {
                    reference[index] = object[index];
                }
            }
        }

        if (object['NS.keys']) {
            object['NS.keys'].forEach((keyObj, index) => {
                const key = getReferenceById(keyObj.UID);
                reference[key] = getReferenceById(object['NS.objects'][index].UID);
            });
        }

        return reference;
    }
}