@pnp/spfx-property-controls
Version:
Reusable property pane controls for SharePoint Framework solutions
322 lines (320 loc) • 11.4 kB
JavaScript
/**
* Helper class to format the CSS code.
* Based on code initially developed by: http://jsbeautifier.org/
*
* Usage:
css_beautify(source_text);
css_beautify(source_text, options);
The options are (default in brackets):
indent_size (4) - indentation size,
indent_char (space) - character to indent with,
selector_separator_newline (true) - separate selectors with newline or
not (e.g. "a,\nbr" or "a, br")
end_with_newline (false) - end with a newline
e.g:
css_beautify(css_source_text, {
'indent_size': 1,
'indent_char': '\t',
'selector_separator': ' ',
'end_with_newline': false,
});
*/
export class CSSFormatter {
constructor() {
this.pos = -1;
this.whiteRe = /^\s+$/; // tokenizer
this.indentLevel = 0;
this.nestedLevel = 0;
this.output = [];
this.print = [];
// https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
this.cssBeautifyNestedAtRule = {
"@page": true,
"@font-face": true,
"@keyframes": true,
// also in cssBeautifyConditionalGroupRule below
"@media": true,
"@supports": true,
"@document": true
};
this.cssBeautifyConditionalGroupRule = {
"@media": true,
"@supports": true,
"@document": true
};
}
next() {
this.ch = this.source_text.charAt(++this.pos);
return this.ch;
}
peek() {
return this.source_text.charAt(this.pos + 1);
}
eatString(endChar) {
const start = this.pos;
while (this.next()) {
if (this.ch === "\\") {
this.next();
this.next();
}
else if (this.ch === endChar) {
break;
}
else if (this.ch === "\n") {
break;
}
}
return this.source_text.substring(start, this.pos + 1);
}
eatWhitespace() {
const start = this.pos;
while (this.whiteRe.test(this.peek())) {
this.pos++;
}
return this.pos !== start;
}
skipWhitespace() {
const start = this.pos;
do {
// no-op;
} while (this.whiteRe.test(this.next()));
return this.pos !== start + 1;
}
eatComment(singleLine) {
const start = this.pos;
this.next();
while (this.next()) {
if (this.ch === "*" && this.peek() === "/") {
this.pos++;
break;
}
else if (singleLine && this.ch === "\n") {
break;
}
}
return this.source_text.substring(start, this.pos + 1);
}
lookBack(str) {
return this.source_text.substring(this.pos - str.length, this.pos).toLowerCase() === str;
}
isCommentOnLine() {
const endOfLine = this.source_text.indexOf('\n', this.pos);
if (endOfLine === -1) {
return false;
}
const restOfLine = this.source_text.substring(this.pos, endOfLine);
return restOfLine.indexOf('//') !== -1;
}
indent() {
this.indentLevel++;
this.indentString += this.singleIndent;
}
outdent() {
this.indentLevel--;
this.indentString = this.indentString.slice(0, -this.indentSize);
}
lastCharWhitespace() {
return this.whiteRe.test(this.output[this.output.length - 1]);
}
newLine(keepWhitespace) {
if (!keepWhitespace) {
while (this.lastCharWhitespace()) {
this.output.pop();
}
}
if (this.output.length) {
this.output.push('\n');
}
if (this.indentString) {
this.output.push(this.indentString);
}
}
singleSpace() {
if (this.output.length && !this.lastCharWhitespace()) {
this.output.push(' ');
}
}
css_beautify(sourceText, options) {
options = options || {};
this.source_text = sourceText;
let indentSize = options.indent_size || 4;
const indentCharacter = options.indent_char || ' ';
const selectorSeparatorNewline = (options.selector_separator_newline === undefined) ? true : options.selector_separator_newline;
const endWithNewline = (options.end_with_newline === undefined) ? false : options.end_with_newline;
// compatibility
if (typeof indentSize === "string") {
indentSize = parseInt(indentSize, 10);
}
this.indentString = sourceText.match(/^[\r\n]*[\t ]*/)[0];
this.singleIndent = new Array(indentSize + 1).join(indentCharacter);
this.print["{"] = (chOpenBrace) => {
this.singleSpace();
this.output.push(chOpenBrace);
this.newLine(false);
};
this.print["}"] = (chCloseBrace) => {
this.newLine(false);
this.output.push(chCloseBrace);
this.newLine(false);
};
if (this.indentString) {
this.output.push(this.indentString);
}
let insideRule = false;
let enteringConditionalGroup = false;
while (true) { // eslint-disable-line no-constant-condition
const isAfterSpace = this.skipWhitespace();
if (!this.ch) {
break;
}
else if (this.ch === '/' && this.peek() === '*') {
/* css comment */
this.newLine(false);
this.output.push(this.eatComment(true), "\n", this.indentString);
const header = this.lookBack("");
if (header) {
this.newLine(false);
}
}
else if (this.ch === '/' && this.peek() === '/') {
// single line comment
this.output.push(this.eatComment(true), this.indentString);
}
else if (this.ch === '@') {
// strip trailing space, if present, for hash property checks
const atRule = this.eatString(" ").replace(/ $/, '');
// pass along the space we found as a separate item
this.output.push(atRule, this.ch);
// might be a nesting at-rule
if (atRule in this.cssBeautifyNestedAtRule) {
this.nestedLevel += 1;
if (atRule in this.cssBeautifyConditionalGroupRule) {
enteringConditionalGroup = true;
}
}
}
else if (this.ch === '{') {
this.eatWhitespace();
if (this.peek() === '}') {
this.next();
this.output.push(" {}");
}
else {
this.indent();
this.print["{"](this.ch);
// when entering conditional groups, only rulesets are allowed
if (enteringConditionalGroup) {
enteringConditionalGroup = false;
insideRule = (this.indentLevel > this.nestedLevel);
}
else {
// otherwise, declarations are also allowed
insideRule = (this.indentLevel >= this.nestedLevel);
}
}
}
else if (this.ch === '}') {
this.outdent();
this.print["}"](this.ch);
insideRule = false;
if (this.nestedLevel) {
this.nestedLevel--;
}
}
else if (this.ch === ":") {
this.eatWhitespace();
if (insideRule || enteringConditionalGroup) {
// 'property: value' delimiter
// which could be in a conditional group query
this.output.push(this.ch, " ");
}
else {
if (this.peek() === ":") {
// pseudo-element
this.next();
this.output.push("::");
}
else {
// pseudo-class
this.output.push(this.ch);
}
}
}
else if (this.ch === '"' || this.ch === '\'') {
this.output.push(this.eatString(this.ch));
}
else if (this.ch === ';') {
if (this.isCommentOnLine()) {
const beforeComment = this.eatString('/');
const comment = this.eatComment(true);
this.output.push(beforeComment, comment.substring(1, comment.length - 1), '\n', this.indentString);
}
else {
this.output.push(this.ch, '\n', this.indentString);
}
}
else if (this.ch === '(') {
// may be a url
if (this.lookBack("url")) {
this.output.push(this.ch);
this.eatWhitespace();
this.ch = this.next();
if (this.ch) {
if (this.ch !== ')' && this.ch !== '"' && this.ch !== '\'') {
this.output.push(this.eatString(')'));
}
else {
this.pos--;
}
}
}
else {
if (isAfterSpace) {
this.singleSpace();
}
this.output.push(this.ch);
this.eatWhitespace();
}
}
else if (this.ch === ')') {
this.output.push(this.ch);
}
else if (this.ch === ',') {
this.eatWhitespace();
this.output.push(this.ch);
if (!insideRule && selectorSeparatorNewline) {
this.newLine(false);
}
else {
this.singleSpace();
}
}
else if (this.ch === ']') {
this.output.push(this.ch);
}
else if (this.ch === '[' || this.ch === '=') {
// no whitespace before or after
this.eatWhitespace();
this.output.push(this.ch);
}
else {
if (isAfterSpace) {
this.singleSpace();
}
this.output.push(this.ch);
}
}
let sweetCode = this.output.join('').replace(/[\n ]+$/, '');
// establish end_with_newline
const should = endWithNewline;
const actually = /\n$/.test(sweetCode);
if (should && !actually) {
sweetCode += "\n";
}
else if (!should && actually) {
sweetCode = sweetCode.slice(0, -1);
}
return sweetCode;
}
}
//# sourceMappingURL=CSSFormatter.js.map