node-sass-json-functions
Version:
JSON encode and decode functions for node-sass.
314 lines (293 loc) • 8.8 kB
JavaScript
import * as sass from 'sass';
import rgbHex from 'rgb-hex';
import shortHexColor from 'shorten-css-hex';
import isPlainObject from 'is-plain-obj';
import parseColor from 'parse-color';
import parseUnit from 'parse-css-dimension';
import { OrderedMap } from 'immutable';
import { parse } from 'postcss-values-parser';
/**
* @typedef {import('../index').JsonValue} JsonValue
* @typedef {import('../index').JsonObject} JsonObject
* @typedef {import('../index').JsonArray} JsonArray
*/
/**
* @param {sass.Value|undefined} value
*/
function getJsonValueFromSassValue(value) {
var _resolvedValue;
let resolvedValue;
if (value instanceof sass.SassList) {
resolvedValue = listToArray(value);
} else if (value instanceof sass.SassMap) {
resolvedValue = mapToObject(value);
} else if (value instanceof sass.SassColor) {
/** @type {[number, number, number]} */
const rgbValue = [value.red, value.green, value.blue];
const alphaValue = value.alpha;
if (alphaValue === 1) {
resolvedValue = shortHexColor(`#${rgbHex.apply(null, rgbValue)}`);
} else {
resolvedValue = `rgba(${rgbValue.join(',')},${alphaValue})`;
}
} else if (value instanceof sass.SassNumber) {
if (value.hasUnits) {
resolvedValue = String(value.value) + value.numeratorUnits.last();
} else {
resolvedValue = Number(value.value);
}
} else if (value instanceof sass.SassString) {
resolvedValue = String(value.text);
} else if (value instanceof sass.SassBoolean) {
resolvedValue = Boolean(value.value);
} else if (typeof value === 'undefined') {
resolvedValue = null;
} else if (value.isTruthy) {
resolvedValue = value.toString();
} else if (value.realNull) {
resolvedValue = null;
}
return (_resolvedValue = resolvedValue) !== null && _resolvedValue !== void 0 ? _resolvedValue : null;
}
/**
* @param {sass.SassList} list
*/
function listToArray(list) {
const length = list.asList.size;
/** @type {JsonArray} */
const data = [];
for (const index of Array.from({
length
}).keys()) {
const value = getJsonValueFromSassValue(list.get(index));
data.push(value);
}
return data;
}
/**
* @param {sass.SassMap} map
*/
function mapToObject(map) {
const length = map.contents.size;
/** @type {JsonObject} */
const data = {};
for (const index of Array.from({
length
}).keys()) {
const resolvedValue = map.get(index);
if (typeof resolvedValue !== 'undefined') {
const key = String(getJsonValueFromSassValue(resolvedValue.get(0)));
const value = getJsonValueFromSassValue(resolvedValue.get(1));
data[key] = value;
}
}
return data;
}
/**
* @typedef {import('../index').JsonValue} JsonValue
* @typedef {import('../index').JsonObject} JsonObject
* @typedef {import('../index').JsonArray} JsonArray
*/
const unitTypes = ['length', 'angle', 'resolution', 'frequency', 'time'];
/**
* @param {string} value
*/
function isColor(value) {
return typeof parseColor(value).rgba !== 'undefined';
}
/**
* @param {string} value
*/
function isCalculation(value) {
if (!value.includes('calc')) {
return false;
}
try {
const root = parse(value);
const node = root.first;
return (node === null || node === void 0 ? void 0 : node.type) === 'func' && node.name === 'calc';
} catch {
return false;
}
}
/**
* @param {string} value
* @param {{forceNumber: boolean}=} options
*/
function parseValueToStringOrNumber(value, options) {
const {
forceNumber = false
} = options !== null && options !== void 0 ? options : {};
let resolvedValue;
try {
const {
value: parsedValue,
unit,
type
} = parseUnit(value);
if (unitTypes.includes(type)) {
resolvedValue = new sass.SassNumber(parsedValue, unit);
} else if (type === 'percentage') {
resolvedValue = new sass.SassNumber(parsedValue, '%');
} else if (forceNumber && !Number.isNaN(Number(value))) {
resolvedValue = new sass.SassNumber(Number(value));
} else {
resolvedValue = new sass.SassString(value);
}
} catch (error) {
resolvedValue = new sass.SassString(value);
}
return resolvedValue;
}
/**
* @param {string} value
*/
function parseValueToColor(value) {
const [red, green, blue, alpha] = parseColor(value).rgba;
return new sass.SassColor({
red,
green,
blue,
alpha
});
}
/**
* @typedef {object} CalculationContainer
* @property {?sass.CalculationOperator} operator
* @property {?sass.CalculationValue} left
* @property {?sass.CalculationValue} right
*/
/**
* @param {string} value
*/
function parseValueToCalculation(value) {
const root = parse(value);
const node = root.first;
let calc = /** @type {CalculationContainer} */{
operator: null,
left: null,
right: null
};
if ((node === null || node === void 0 ? void 0 : node.type) === 'func' && node.name === 'calc') {
if (node.nodes.length > 3) {
return parseValueToStringOrNumber(value);
}
node.nodes.forEach((node, index) => {
if (node.type === 'operator') {
calc.operator = /** @type {sass.CalculationOperator} */
node.value;
} else if (index === 0) {
calc.left = parseValueToStringOrNumber(node.toString());
} else {
calc.right = parseValueToStringOrNumber(node.toString(), {
forceNumber: true
});
}
});
}
try {
return sass.SassCalculation.calc(new sass.CalculationOperation(
// @ts-ignore
calc.operator, calc.left, calc.right));
} catch {
return parseValueToStringOrNumber(value);
}
}
/**
* @param {JsonValue} value
*/
function setJsonValueToSassValue(value) {
let resolvedValue;
if (Array.isArray(value)) {
resolvedValue = arrayToList(value);
} else if (isPlainObject(value)) {
resolvedValue = objectToMap(value);
} else if (isColor(String(value))) {
resolvedValue = parseValueToColor(String(value));
} else if (isCalculation(String(value))) {
resolvedValue = parseValueToCalculation(String(value));
} else if (typeof value === 'string') {
resolvedValue = parseValueToStringOrNumber(value);
} else if (typeof value === 'number') {
resolvedValue = new sass.SassNumber(value);
} else if (typeof value === 'boolean') {
resolvedValue = value ? sass.sassTrue : sass.sassFalse;
} else {
resolvedValue = sass.sassNull;
}
return resolvedValue;
}
/**
* @param {JsonArray} array
*/
function arrayToList(array) {
/** @type {sass.Value[]} */
const data = [];
for (const item of array) {
data.push(setJsonValueToSassValue(item));
}
return new sass.SassList(data);
}
/**
* @param {JsonObject} object
*/
function objectToMap(object) {
/** @type {[sass.Value, sass.Value][]} */
const data = [];
for (const [property, value = null] of Object.entries(object)) {
data.push([setJsonValueToSassValue(property), setJsonValueToSassValue(value)]);
}
// eslint-disable-next-line new-cap
return new sass.SassMap(OrderedMap(data));
}
/**
* @typedef {JsonPrimitive | JsonObject | JsonArray} JsonValue
* @typedef {JsonValue[]} JsonArray
* @typedef {string | number | boolean | null} JsonPrimitive
* @typedef {{[Key in string]?: JsonValue}} JsonObject
*/
/**
* Encodes (`JSON.stringify`) data and returns Sass string. By default, string is quoted with single quotes so that it can be easily used in standard CSS values.
*
* First argument: `sass.Value` - Data to encode (stringify).
*
* Second argument: `sass.SassBoolean` - Should output string be quoted with single quotes.
*
* @param {sass.Value[]} encodeArguments
*/
function encode(encodeArguments) {
const [data, quotes_] = encodeArguments;
const quotes = quotes_.assertBoolean('quotes');
const shouldQuote = quotes.value;
let resolvedValue = JSON.stringify(getJsonValueFromSassValue(data));
if (shouldQuote) {
resolvedValue = `'${resolvedValue}'`;
}
return new sass.SassString(resolvedValue);
}
/**
* Decodes (`JSON.parse`) string and returns one of available Sass types.
*
* First argument: `sass.SassString` - String to decode (parse).
*
* @param {sass.Value[]} decodeArguments
*/
function decode(decodeArguments) {
const [string_] = decodeArguments;
const string = string_.assertString('string');
/** @type {JsonValue?} */
let resolvedValue = {};
try {
resolvedValue = JSON.parse(string.text);
} catch (error) {
resolvedValue = null;
}
return setJsonValueToSassValue(resolvedValue);
}
/** @type {{ 'json-encode($data, $quotes: true)': typeof encode, 'json-decode($string)': typeof decode }} */
const api = {
'json-encode($data, $quotes: true)': encode,
'json-decode($string)': decode
};
export { api as default };
//# sourceMappingURL=index.js.map