refactor(ivy): create better styling parsing API (#34418)

Parsing styling is now simplified to be used like so:
```
for (let i = parseStyle(text); i <= 0; i = parseStyleNext(text, i)) {
  const key = getLastParsedKey();
  const value = getLastParsedValue();
  ...
}
```

This change makes it easier to invoke the parser from other locations in the system without paying the cost of creating and iterating over `Map` of styles.

PR Closes #34418
This commit is contained in:
Misko Hevery 2019-12-15 21:05:24 -08:00 committed by Miško Hevery
parent da7e362bce
commit 54af220107
4 changed files with 204 additions and 78 deletions

View File

@ -7,7 +7,9 @@
*/ */
import {CharCode} from '../../util/char_code'; import {CharCode} from '../../util/char_code';
import {consumeClassToken, consumeWhitespace} from './styling_parser';
import {consumeWhitespace, getLastParsedKey, parseClassName, parseClassNameNext} from './styling_parser';
/** /**
* Computes the diff between two class-list strings. * Computes the diff between two class-list strings.
@ -50,15 +52,8 @@ export function computeClassChanges(oldValue: string, newValue: string): Map<str
*/ */
export function splitClassList( export function splitClassList(
text: string, changes: Map<string, boolean|null>, isNewValue: boolean): void { text: string, changes: Map<string, boolean|null>, isNewValue: boolean): void {
const end = text.length; for (let i = parseClassName(text); i >= 0; i = parseClassNameNext(text, i)) {
let index = 0; processClassToken(changes, getLastParsedKey(text), isNewValue);
while (index < end) {
index = consumeWhitespace(text, index, end);
const tokenEnd = consumeClassToken(text, index, end);
if (tokenEnd !== index) {
processClassToken(changes, text.substring(index, tokenEnd), isNewValue);
}
index = tokenEnd;
} }
} }

View File

@ -6,10 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {CharCode} from '../../util/char_code'; import {getLastParsedKey, getLastParsedValue, getLastParsedValueEnd, parseStyle, parseStyleNext, resetParserState} from './styling_parser';
import {consumeSeparator, consumeStyleKey, consumeStyleValue, consumeWhitespace} from './styling_parser';
/** /**
* Stores changes to Style values. * Stores changes to Style values.
@ -60,24 +57,8 @@ export function computeStyleChanges(oldValue: string, newValue: string): StyleCh
* @param isNewValue `true` if parsing new value (effects how values get added to `changes`) * @param isNewValue `true` if parsing new value (effects how values get added to `changes`)
*/ */
export function parseKeyValue(text: string, changes: StyleChangesMap, isNewValue: boolean): void { export function parseKeyValue(text: string, changes: StyleChangesMap, isNewValue: boolean): void {
const end = text.length; for (let i = parseStyle(text); i >= 0; i = parseStyleNext(text, i)) {
let start = 0; processStyleKeyValue(changes, getLastParsedKey(text), getLastParsedValue(text), isNewValue);
while (start < end) {
const keyStart = consumeWhitespace(text, start, end);
const keyEnd = consumeStyleKey(text, keyStart, end);
if (keyEnd === keyStart) {
// we reached an end so just quit
break;
}
const valueStart = consumeSeparator(text, keyEnd, end, CharCode.COLON);
const valueEnd = consumeStyleValue(text, valueStart, end);
if (ngDevMode && valueStart === valueEnd) {
throw malformedStyleError(text, valueStart);
}
start = consumeSeparator(text, valueEnd, end, CharCode.SEMI_COLON);
const key = text.substring(keyStart, keyEnd);
const value = text.substring(valueStart, valueEnd);
processStyleKeyValue(changes, key, value, isNewValue);
} }
} }
@ -122,51 +103,26 @@ function styleKeyValue(oldValue: string | null, newValue: string | null) {
* @returns a new style text which does not have `styleToRemove` (and its value) * @returns a new style text which does not have `styleToRemove` (and its value)
*/ */
export function removeStyle(cssText: string, styleToRemove: string): string { export function removeStyle(cssText: string, styleToRemove: string): string {
let start = 0; if (cssText.indexOf(styleToRemove) === -1) {
let end = cssText.length; // happy case where we don't need to invoke parser.
return cssText;
}
let lastValueEnd = 0; let lastValueEnd = 0;
while (start < end) { for (let i = parseStyle(cssText); i >= 0; i = parseStyleNext(cssText, i)) {
const possibleKeyIndex = cssText.indexOf(styleToRemove, start); const key = getLastParsedKey(cssText);
if (possibleKeyIndex === -1) { if (key === styleToRemove) {
// we did not find anything, so just bail. if (lastValueEnd === 0) {
break; cssText = cssText.substring(i);
} i = 0;
while (start < possibleKeyIndex + 1) { } else if (i === cssText.length) {
const keyStart = consumeWhitespace(cssText, start, end); return cssText.substring(0, lastValueEnd);
const keyEnd = consumeStyleKey(cssText, keyStart, end);
if (keyEnd === keyStart) {
// we reached the end
return cssText;
}
const valueStart = consumeSeparator(cssText, keyEnd, end, CharCode.COLON);
const valueEnd = consumeStyleValue(cssText, valueStart, end);
if (ngDevMode && valueStart === valueEnd) {
throw malformedStyleError(cssText, valueStart);
}
const valueEndSep = consumeSeparator(cssText, valueEnd, end, CharCode.SEMI_COLON);
if (keyStart == possibleKeyIndex && keyEnd === possibleKeyIndex + styleToRemove.length) {
if (valueEndSep == end) {
// This is a special case when we are the last key in a list, we then chop off the
// trailing separator as well.
cssText = cssText.substring(0, lastValueEnd);
} else {
cssText = cssText.substring(0, keyStart) + cssText.substring(valueEndSep, end);
}
end = cssText.length;
start = keyStart;
break; // rescan.
} else { } else {
// This was not the item we are looking for, keep going. cssText = cssText.substring(0, lastValueEnd) + '; ' + cssText.substring(i);
start = valueEndSep; i = lastValueEnd + 2; // 2 is for '; '.length(so that we skip the separator)
} }
lastValueEnd = valueEnd; resetParserState(cssText);
} }
lastValueEnd = getLastParsedValueEnd();
} }
return cssText; return cssText;
} }
function malformedStyleError(text: string, index: number) {
return new Error(
`Malformed style at location ${index} in string '` + text.substring(0, index) + '[>>' +
text.substring(index, index + 1) + '<<]' + text.substr(index + 1) + '\'.');
}

View File

@ -8,6 +8,174 @@
import {CharCode} from '../../util/char_code'; import {CharCode} from '../../util/char_code';
/**
* Stores the locations of key/value indexes while parsing styling.
*
* In case of `cssText` parsing the indexes are like so:
* ```
* "key1: value1; key2: value2; key3: value3"
* ^ ^ ^ ^ ^
* | | | | +-- textEnd
* | | | +---------------- valueEnd
* | | +---------------------- value
* | +------------------------ keyEnd
* +---------------------------- key
* ```
*
* In case of `className` parsing the indexes are like so:
* ```
* "key1 key2 key3"
* ^ ^ ^
* | | +-- textEnd
* | +------------------------ keyEnd
* +---------------------------- key
* ```
* NOTE: `value` and `valueEnd` are used only for styles, not classes.
*/
interface ParserState {
textEnd: number;
key: number;
keyEnd: number;
value: number;
valueEnd: number;
}
// Global state of the parser. (This makes parser non-reentrant, but that is not an issue)
const parserState: ParserState = {
textEnd: 0,
key: 0,
keyEnd: 0,
value: 0,
valueEnd: 0,
};
/**
* Retrieves the last parsed `key` of style.
* @param text the text to substring the key from.
*/
export function getLastParsedKey(text: string): string {
return text.substring(parserState.key, parserState.keyEnd);
}
/**
* Retrieves the last parsed `value` of style.
* @param text the text to substring the key from.
*/
export function getLastParsedValue(text: string): string {
return text.substring(parserState.value, parserState.valueEnd);
}
/**
* Initializes `className` string for parsing and parses the first token.
*
* This function is intended to be used in this format:
* ```
* for (let i = parseClassName(text); i >= 0; i = parseClassNameNext(text, i))) {
* const key = getLastParsedKey();
* ...
* }
* ```
* @param text `className` to parse
* @returns index where the next invocation of `parseClassNameNext` should resume.
*/
export function parseClassName(text: string): number {
resetParserState(text);
return parseClassNameNext(text, consumeWhitespace(text, 0, parserState.textEnd));
}
/**
* Parses next `className` token.
*
* This function is intended to be used in this format:
* ```
* for (let i = parseClassName(text); i >= 0; i = parseClassNameNext(text, i))) {
* const key = getLastParsedKey();
* ...
* }
* ```
*
* @param text `className` to parse
* @param index where the parsing should resume.
* @returns index where the next invocation of `parseClassNameNext` should resume.
*/
export function parseClassNameNext(text: string, index: number): number {
const end = parserState.textEnd;
if (end === index) {
return -1;
}
index = parserState.keyEnd = consumeClassToken(text, parserState.key = index, end);
return consumeWhitespace(text, index, end);
}
/**
* Initializes `cssText` string for parsing and parses the first key/values.
*
* This function is intended to be used in this format:
* ```
* for (let i = parseStyle(text); i >= 0; i = parseStyleNext(text, i))) {
* const key = getLastParsedKey();
* const value = getLastParsedValue();
* ...
* }
* ```
* @param text `cssText` to parse
* @returns index where the next invocation of `parseStyleNext` should resume.
*/
export function parseStyle(text: string): number {
resetParserState(text);
return parseStyleNext(text, consumeWhitespace(text, 0, parserState.textEnd));
}
/**
* Parses the next `cssText` key/values.
*
* This function is intended to be used in this format:
* ```
* for (let i = parseStyle(text); i >= 0; i = parseStyleNext(text, i))) {
* const key = getLastParsedKey();
* const value = getLastParsedValue();
* ...
* }
*
* @param text `cssText` to parse
* @param index where the parsing should resume.
* @returns index where the next invocation of `parseStyleNext` should resume.
*/
export function parseStyleNext(text: string, startIndex: number): number {
const end = parserState.textEnd;
if (end === startIndex) {
// we reached an end so just quit
return -1;
}
let index = parserState.keyEnd = consumeStyleKey(text, parserState.key = startIndex, end);
index = parserState.value = consumeSeparatorWithWhitespace(text, index, end, CharCode.COLON);
index = parserState.valueEnd = consumeStyleValue(text, index, end);
if (ngDevMode && parserState.value === parserState.valueEnd) {
throw malformedStyleError(text, index);
}
return consumeSeparatorWithWhitespace(text, index, end, CharCode.SEMI_COLON);
}
/**
* Reset the global state of the styling parser.
* @param text The styling text to parse.
*/
export function resetParserState(text: string): void {
parserState.key = 0;
parserState.keyEnd = 0;
parserState.value = 0;
parserState.valueEnd = 0;
parserState.textEnd = text.length;
}
/**
* Retrieves tha `valueEnd` from the parser global state.
*
* See: `ParserState`.
*/
export function getLastParsedValueEnd(): number {
return parserState.valueEnd;
}
/** /**
* Returns index of next non-whitespace character. * Returns index of next non-whitespace character.
* *
@ -65,7 +233,7 @@ export function consumeStyleKey(text: string, startIndex: number, endIndex: numb
* @param endIndex Ending index of character where the scan should end. * @param endIndex Ending index of character where the scan should end.
* @returns Index after separator and surrounding whitespace. * @returns Index after separator and surrounding whitespace.
*/ */
export function consumeSeparator( export function consumeSeparatorWithWhitespace(
text: string, startIndex: number, endIndex: number, separator: number): number { text: string, startIndex: number, endIndex: number, separator: number): number {
startIndex = consumeWhitespace(text, startIndex, endIndex); startIndex = consumeWhitespace(text, startIndex, endIndex);
if (startIndex < endIndex) { if (startIndex < endIndex) {
@ -150,4 +318,10 @@ function expectingError(text: string, expecting: string, index: number) {
return new Error( return new Error(
`Expecting '${expecting}' at location ${index} in string '` + text.substring(0, index) + `Expecting '${expecting}' at location ${index} in string '` + text.substring(0, index) +
'[>>' + text.substring(index, index + 1) + '<<]' + text.substr(index + 1) + '\'.'); '[>>' + text.substring(index, index + 1) + '<<]' + text.substr(index + 1) + '\'.');
} }
function malformedStyleError(text: string, index: number) {
return new Error(
`Malformed style at location ${index} in string '` + text.substring(0, index) + '[>>' +
text.substring(index, index + 1) + '<<]' + text.substr(index + 1) + '\'.');
}

View File

@ -7,7 +7,7 @@
*/ */
import {StyleChangesMap, parseKeyValue, removeStyle} from '@angular/core/src/render3/styling/style_differ'; import {StyleChangesMap, parseKeyValue, removeStyle} from '@angular/core/src/render3/styling/style_differ';
import {consumeSeparator, consumeStyleValue} from '@angular/core/src/render3/styling/styling_parser'; import {consumeSeparatorWithWhitespace, consumeStyleValue} from '@angular/core/src/render3/styling/styling_parser';
import {CharCode} from '@angular/core/src/util/char_code'; import {CharCode} from '@angular/core/src/util/char_code';
import {sortedForEach} from './class_differ_spec'; import {sortedForEach} from './class_differ_spec';
@ -92,6 +92,7 @@ describe('style differ', () => {
it('should remove some of the style', () => { it('should remove some of the style', () => {
expect(removeStyle('a: a; foo: bar; b: b', 'foo')).toEqual('a: a; b: b'); expect(removeStyle('a: a; foo: bar; b: b', 'foo')).toEqual('a: a; b: b');
expect(removeStyle('a: a; foo: bar; b: b', 'foo')).toEqual('a: a; b: b');
expect(removeStyle('a: a; foo: bar; b: b; foo: bar; c: c', 'foo')) expect(removeStyle('a: a; foo: bar; b: b; foo: bar; c: c', 'foo'))
.toEqual('a: a; b: b; c: c'); .toEqual('a: a; b: b; c: c');
}); });
@ -113,9 +114,9 @@ function expectParseValue(
text: string) { text: string) {
let stopIndex = text.indexOf('🛑'); let stopIndex = text.indexOf('🛑');
if (stopIndex < 0) stopIndex = text.length; if (stopIndex < 0) stopIndex = text.length;
const valueStart = consumeSeparator(text, 0, text.length, CharCode.COLON); const valueStart = consumeSeparatorWithWhitespace(text, 0, text.length, CharCode.COLON);
const valueEnd = consumeStyleValue(text, valueStart, text.length); const valueEnd = consumeStyleValue(text, valueStart, text.length);
const valueSep = consumeSeparator(text, valueEnd, text.length, CharCode.SEMI_COLON); const valueSep = consumeSeparatorWithWhitespace(text, valueEnd, text.length, CharCode.SEMI_COLON);
expect(valueSep).toBe(stopIndex); expect(valueSep).toBe(stopIndex);
return expect(text.substring(valueStart, valueEnd)); return expect(text.substring(valueStart, valueEnd));
} }