Skip to content

Commit

Permalink
Merge pull request #189 from stylify/mangling
Browse files Browse the repository at this point in the history
Mangling
  • Loading branch information
Machy8 authored Feb 9, 2023
2 parents 3dfd0be + 6d5ff3c commit 63786ec
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 88 deletions.
160 changes: 79 additions & 81 deletions packages/stylify/src/Compiler/Compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '.';

import { hooks } from '../Hooks';
import { escapeCssSelector, getStringOriginalStateAfterReplace, prepareStringForReplace, tokenize } from '../Utilities';

export interface CompilerContentOptionsInterface {
pregenerate: PregenerateType,
Expand Down Expand Up @@ -96,8 +97,11 @@ export interface CompilerConfigInterface {
matchCustomSelectors?: boolean
}

export type CustomSelectorTypeType = 'custom' | 'customMatchedInClass' | 'component' | 'utilitiesGroup';

export interface CustomSelectorsInterface {
customSelectors: CustomSelector[]
customSelectors: CustomSelector[],
type: CustomSelectorTypeType
}

export interface ComponentGeneratorFunctionDataInterface {
Expand All @@ -119,10 +123,6 @@ export class Compiler {

private readonly textPlaceholder = '_TEXT_';

private readonly dollarPlaceholder = '_DOLLAR_';

private readonly backslashPlaceholder = '_BACKSLASH_';

private readonly variableRegExp = /\$([\w-_]+)/g;

private readonly contentOptionsRegExp = /stylify-([a-zA-Z-_0-9]+)\s([\s\S]+?)\s\/stylify-[a-zA-Z-_0-9]+/;
Expand Down Expand Up @@ -254,15 +254,21 @@ export class Compiler {
}
}

public addCustomSelector(selector: string, selectors: string, selectorCanBeSplit = true): void {
public addCustomSelector(
selector: string,
selectors: string,
selectorCanBeSplit = true,
type: CustomSelectorTypeType = 'custom'
): void {
if (selectorCanBeSplit && selector.includes(',')) {
selector.split(',').forEach((selectorSplit) => this.addCustomSelector(selectorSplit.trim(), selectors));
return;
}

if (!(selector in this.customSelectors)) {
this.customSelectors[selector] = {
customSelectors: []
customSelectors: [],
type
};
}

Expand Down Expand Up @@ -330,8 +336,6 @@ export class Compiler {
return content;
}

const dollarPlaceholderRegExp = new RegExp(this.dollarPlaceholder, 'g');
const backslashPlaceholderRegExp = new RegExp(this.backslashPlaceholder, 'g');
const placeholderTextPart = this.textPlaceholder;
const contentPlaceholders: Record<string, string> = {};

Expand All @@ -341,15 +345,7 @@ export class Compiler {
return placeholderKey;
};

// This replaces special characters used within regular expression
// so their are not processed during replacing
// $ => because $$ causes $
// \ => because \u or \v for example are predefined character sets
const replaceSpecialCharacters = (content: string) => content
.replace(/\$/g, this.dollarPlaceholder)
.replace(/\\/g, this.backslashPlaceholder);

content = replaceSpecialCharacters(content)
content = prepareStringForReplace(content)
.replace(new RegExp(this.ignoredAreasRegExpString, 'g'), (...args): string => {
const matchArguments = args.filter((value) => typeof value === 'string');
const fullMatch: string = matchArguments[0];
Expand All @@ -368,7 +364,7 @@ export class Compiler {
.sort((a: string, b: string): number => b.length - a.length);

for (const selector of selectorsListKeys) {
let selectorToReplace = replaceSpecialCharacters(selector);
let selectorToReplace = prepareStringForReplace(selector);

if (!content.includes(selectorToReplace)) {
continue;
Expand All @@ -380,10 +376,8 @@ export class Compiler {
: '';

selectorToReplace = selectorToReplace.replace(/\\/g, '\\\\');
selectorToReplace = this.escapeCssSelector(
minifiedSelectorGenerator.getStringToMatch(
selectorToReplace, matchSelectorsWithPrefixes
)
selectorToReplace = escapeCssSelector(
minifiedSelectorGenerator.getStringToMatch(selectorToReplace, matchSelectorsWithPrefixes)
);

const replacement = `${selectorPrefix}${mangledSelector}`;
Expand All @@ -409,9 +403,7 @@ export class Compiler {
content = content.replace(placeholderKey, contentPlaceholder);
}

content = content
.replace(dollarPlaceholderRegExp, '$$')
.replace(backslashPlaceholderRegExp, '\\');
content = getStringOriginalStateAfterReplace(content);

return content;
}
Expand Down Expand Up @@ -466,9 +458,10 @@ export class Compiler {
: fullMatch;

this.addCustomSelector(
`.${this.escapeCssSelector(customSelectorSelector, true)}`,
customSelectorSelector,
`${customSelector}{${stylifySelectors.replace(/;/g, ' ')}}`,
false
false,
'customMatchedInClass'
);

return '';
Expand All @@ -483,11 +476,12 @@ export class Compiler {
: fullMatch;

this.addCustomSelector(
`.${this.escapeCssSelector(customSelectorSelector, true)}`,
customSelectorSelector,
stylifySelectors.split(';')
.map((stylifySelector) => `${screenAndPseudoClasses}:${stylifySelector}`)
.join(' '),
false
false,
'utilitiesGroup'
);

return '';
Expand Down Expand Up @@ -543,28 +537,6 @@ export class Compiler {
return contentOptions as Data;
}

private escapeCssSelector(selector: string, all = false): string {
const selectorLength = selector.length;
let result = '';
const regExp = all ? /[^a-zA-Z0-9]/ : /[.*+?^${}()|[\]\\]/;

for (let i = 0; i < selectorLength; i++) {
const char = selector[i];
const escapeCharacter = '\\';
if (['-', '_'].includes(char)
|| !regExp.test(char)
|| selector[i - 1] === escapeCharacter
) {
result += char;
continue;
}

result += `\\${char}`;
}

return result;
}

private configureCompilationResult(compilationResult: CompilationResult): CompilationResult
{
const newLine = this.dev ? '\n' : '';
Expand Down Expand Up @@ -658,31 +630,66 @@ export class Compiler {
const selectorsMap: Record<string, string> = {};

for (const [selector, config] of Object.entries(this.customSelectors)) {
const isComponent = config.type === 'component';
const isUtilitiesGroup = config.type === 'utilitiesGroup';
const isCustomSelectorMatchedInClass = config.type === 'customMatchedInClass';
const isClassSelector = isComponent || isUtilitiesGroup || isCustomSelectorMatchedInClass;
const preparedEscapedSelector = config.type === 'custom'
? selector
: escapeCssSelector(selector, isComponent || isUtilitiesGroup || isCustomSelectorMatchedInClass);

for (const customSelector of config.customSelectors) {
const generatedCssEntries = customSelector.generateSelectors(selector);
const generatedCssEntries = customSelector.generateSelectors(
`${isClassSelector ? '.':''}${preparedEscapedSelector}`
);

for (const [customSelector, customSelectorSelectors] of Object.entries(generatedCssEntries)) {
if (!(customSelector in selectorsMap)) {
selectorsMap[customSelector] = '';
}
let cssSelector: string;
let stylifySelectors: string;
for ([cssSelector, stylifySelectors] of Object.entries(generatedCssEntries)) {
if (this.mangleSelectors && isComponent) {
const preparedCssSelector = prepareStringForReplace(cssSelector);
const preparedSelector = prepareStringForReplace(preparedEscapedSelector);

cssSelector = preparedCssSelector.replace(
new RegExp(`\\.(${preparedSelector}\\S*)`),
(fullMatch: string, componentSelector: string) => {
let clearedComponentName = '';
const tokenizableComponentSelector = getStringOriginalStateAfterReplace(
componentSelector
);

const selectorsToAdd = selectorsMap[customSelector].length
? selectorsMap[customSelector].split(' ')
: [];
tokenize(tokenizableComponentSelector, ({ token, previousToken }) => {
const isCorrectToken = (/\w|-|\\|\$/).test(token);

customSelectorSelectors.split(' ').forEach((selector) => {
if (!selectorsToAdd.includes(selector)) {
selectorsToAdd.push(selector);
}
});
if (!isCorrectToken && previousToken !== '\\') {
return true;
}

clearedComponentName += token;
});

return fullMatch.substring(1).replace(
prepareStringForReplace(clearedComponentName),
`.${minifiedSelectorGenerator.getMangledSelector(clearedComponentName)}`
);
});
}

selectorsMap[
this.rewriteSelectors({
content: customSelector,
if (this.mangleSelectors) {
cssSelector = this.rewriteSelectors({
content: cssSelector,
rewriteOnlyInSelectorsAreas: false,
matchSelectorsWithPrefixes: true
});
}

selectorsMap[cssSelector] = [selectorsMap[cssSelector] ?? '', ...stylifySelectors.split(' ')]
.filter((item, index, self) => {
return item.trim().length > 0 && self.indexOf(item) === index;
})
] = selectorsToAdd.filter((item) => item.trim().length > 0).join(' ').replace(/\s/g, ' ').trim();
.join(' ')
.replace(/\s/g, ' ')
.trim();
}
}
}
Expand All @@ -696,11 +703,7 @@ export class Compiler {
let componentMatch: RegExpExecArray;

while ((componentMatch = regExp.exec(content))) {
const componentClassSelector = `.${
this.mangleSelectors
? minifiedSelectorGenerator.getMangledSelector(componentMatch[0])
: this.escapeCssSelector(componentMatch[0], true)
}`;
const componentSelector = componentMatch[0];

for (const selectorsOrGenerator of config.selectorsOrGenerators) {
const componentSelectors = typeof selectorsOrGenerator === 'function'
Expand All @@ -712,14 +715,9 @@ export class Compiler {
})
: selectorsOrGenerator;

const customSelector = new CustomSelector(componentSelectors);
const generatedCssEntries = Object.entries(
customSelector.generateSelectors(componentClassSelector)
);
minifiedSelectorGenerator.getMangledSelector(componentSelector);

for (const [componentCustomSelector, selectorsToAttach] of generatedCssEntries) {
this.addCustomSelector(componentCustomSelector, selectorsToAttach);
}
this.addCustomSelector(componentSelector, componentSelectors, false, 'component');
}
}
}
Expand Down
5 changes: 2 additions & 3 deletions packages/stylify/src/Compiler/CustomSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,8 @@ export class CustomSelector {

} else {
actualTreeSelector += ` ${replaceRootPlaceholder(
selectorIncludesPlaceholder
? selectorSplitPart.substring(1)
: selectorSplitPart, actualTreeSelector
selectorIncludesPlaceholder ? selectorSplitPart.substring(1) : selectorSplitPart,
actualTreeSelector
)}`;
}

Expand Down
1 change: 1 addition & 0 deletions packages/stylify/src/Utilities/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './objects';
export * from './numbers';
export * from './strings';
70 changes: 70 additions & 0 deletions packages/stylify/src/Utilities/strings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
export const dollarPlaceholder = '_DLR_';

export const backslashPlaceholder = '_BSLASH_';

export type TokenizerCallbackType = (data: {
iterator: number,
token: string|undefined,
nextToken: string|undefined,
previousToken: string|undefined,
tokensCount: number,
isLastToken: boolean,
isFirstToken: boolean
}) => boolean|void;

export const escapeCssSelector = (selector: string, all = false) => {
const selectorLength = selector.length;
let result = '';
const regExp = all ? /[^a-zA-Z0-9]/ : /[.*+?^${}()|[\]\\]/;

for (let i = 0; i < selectorLength; i++) {
const char = selector[i];
const escapeCharacter = '\\';

if (['-', '_'].includes(char) || !regExp.test(char) || selector[i - 1] === escapeCharacter) {
result += char;
continue;
}

result += `\\${char}`;
}

return result;
};

export const tokenize = (content: string, tokenizerCallback: TokenizerCallbackType): void => {
const tokens = content.split('');
const tokensCount = tokens.length;

for (let i = 0; i < tokensCount; i ++) {
if (tokenizerCallback({
token: tokens[i],
nextToken: tokens[i + 1],
previousToken: tokens[i - 1],
iterator: i,
isLastToken: i + 1 === tokensCount,
isFirstToken: i === 0,
tokensCount
})) {
break;
}
}
};

const dollarPlaceholderRegExp = new RegExp(dollarPlaceholder, 'g');
const backslashPlaceholderRegExp = new RegExp(backslashPlaceholder, 'g');

export const prepareStringForReplace = (content: string) => {

/*
This replaces special characters used within regular expression
so their are not processed during replacing
$ => because $$ causes $
\ => because \u or \v for example are predefined character sets
*/
return content.replace(/\$/g, dollarPlaceholder).replace(/\\/g, backslashPlaceholder);
};

export const getStringOriginalStateAfterReplace = (content: string) => {
return content.replace(dollarPlaceholderRegExp, '$$').replace(backslashPlaceholderRegExp, '\\');
};
6 changes: 4 additions & 2 deletions packages/stylify/tests/jest/components.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,17 +112,19 @@ test('Components - nested syntax - mangled', (): void => {
...nestedSyntaxCompilerConfig
});

// This order is on purpose.
// This selector uses component .header (below) that was not yet defined.
compiler.addCustomSelector('#wrapper', `
.not-minified-2 { color:darkred }
.header { color:green }
header { color:orange }
`)
`);

compiler.addComponent('header', `
font-style:italic
+ #header { color:purple }
`)
`);

const input = testUtils.getHtmlInputFile(testFileName);
let compilationResult = compiler.compile(input);
Expand Down
Loading

0 comments on commit 63786ec

Please sign in to comment.