Skip to content

Commit 13ff092

Browse files
refactor(css,html): one way to read node types and positions, spec-named helpers (#22633)
* refactor(css): read node types through NodeType instead of T_* aliases * refactor(css,html): read positions through the public path outside the parsers * refactor(css,html): name the useful syntax helpers publicly and drop dead exports * test(css): keep a shape whose parts a substitution may stand for * test(css): keep a quoted string that is a function argument in a font family
1 parent b19519a commit 13ff092

8 files changed

Lines changed: 565 additions & 545 deletions

File tree

‎lib/css/syntax-parser.js‎

Lines changed: 279 additions & 297 deletions
Large diffs are not rendered by default.

‎lib/css/syntax-printer.js‎

Lines changed: 115 additions & 124 deletions
Large diffs are not rendered by default.

‎lib/html/HtmlGenerator.js‎

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -722,29 +722,28 @@ class HtmlGenerator extends Generator {
722722
// `skip.text` populates `contentEnd` (raw-text body end) without
723723
// materializing text nodes; `</script` stays escaped, so CSP hashes match
724724
// the exact bytes the browser sees.
725-
const {
726-
Field,
727-
Flag,
728-
NodeType,
729-
_attrRawValue,
730-
_contentEndOf,
731-
_contentStartOf,
732-
_endOf,
733-
_nameEndOf,
734-
_startOf,
735-
_valueStartOf
736-
} = getHtmlParser();
725+
const { Field, Flag, NodeType, Part } = getHtmlParser();
737726
const { SourceProcessor } = getHtmlSyntax();
727+
/**
728+
* Where an element's content starts; a parser-inserted one has no tag, so
729+
* its content starts where it does.
730+
* @param {import("./syntax-parser").HtmlPath} path the element's path
731+
* @returns {number} the offset
732+
*/
733+
const contentStart = (path) => {
734+
const start = path.startOf(Part.content);
735+
return start === -1 ? path.start() : start;
736+
};
738737
new SourceProcessor()
739738
.use({
740739
[NodeType.Element]: {
741740
enter: (path) => {
742741
const name = path.name();
743742
if (name === "head") {
744-
if (headOpen < 0) headOpen = headEnd = _contentStartOf(path.node);
743+
if (headOpen < 0) headOpen = headEnd = contentStart(path);
745744
headDepth++;
746745
} else if (name === "body") {
747-
if (bodyOpen < 0) bodyOpen = bodyEnd = _contentStartOf(path.node);
746+
if (bodyOpen < 0) bodyOpen = bodyEnd = contentStart(path);
748747
bodyDepth++;
749748
}
750749
},
@@ -761,7 +760,7 @@ class HtmlGenerator extends Generator {
761760
if (bodyDepth > 0) bodyDepth--;
762761
return;
763762
}
764-
const end = _endOf(path.node);
763+
const end = path.end();
765764
if (headDepth > 0 && end > headEnd) headEnd = end;
766765
if (bodyDepth > 0 && end > bodyEnd) bodyEnd = end;
767766
if (
@@ -777,8 +776,11 @@ class HtmlGenerator extends Generator {
777776
const count = path.fieldCount(Field.attributes);
778777
for (let i = 0; i < count; i++) {
779778
const attr = path.field(i, Field.attributes);
779+
const valueStart = path.startOf(Part.value, attr);
780780
attrs[path.name(attr)] =
781-
_valueStartOf(attr) === -1 ? true : _attrRawValue(attr);
781+
valueStart === -1
782+
? true
783+
: path.source(valueStart, path.endOf(Part.value, attr));
782784
}
783785
const isRawText = name === "script" || name === "style";
784786
tags.push({
@@ -787,12 +789,12 @@ class HtmlGenerator extends Generator {
787789
injectTo: bodyDepth > 0 ? "body" : "head"
788790
});
789791
spans.push({
790-
start: _startOf(path.node),
791-
openEnd: _contentStartOf(path.node),
792+
start: path.start(),
793+
openEnd: path.startOf(Part.content),
792794
end,
793795
location: bodyDepth > 0 ? "body" : "head",
794-
nameEnd: isRawText ? _nameEndOf(path.node) : -1,
795-
contentEnd: isRawText ? _contentEndOf(path.node) : -1,
796+
nameEnd: isRawText ? path.endOf(Part.name) : -1,
797+
contentEnd: isRawText ? path.endOf(Part.content) : -1,
796798
selfClosing: path.flag(Flag.selfClosing)
797799
});
798800
originals.push(snapshotAttrs(attrs));

‎lib/html/syntax-parser.js‎

Lines changed: 42 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4930,25 +4930,10 @@ const _closeTagOf = (n) => {
49304930
const _startOf = (n) => (n < 0 ? _attrNameStarts[-n] : _nodeStarts[n]);
49314931
/** @type {(n: HtmlNodeRef) => number} where a node or attribute ends */
49324932
const _endOf = (n) => (n < 0 ? _attrEnd(-n) : _nodeEnds[n]);
4933-
/** @type {(n: HtmlNodeRef) => number} where a tag or attribute name starts (0 for a parser-inserted tag) */
4934-
const _nameStartOf = (n) => {
4935-
if (n < 0) return _attrNameStarts[-n];
4936-
return _nodeNameEnds[n] === 0 ? 0 : _nodeStarts[n] + 1;
4937-
};
49384933
/** @type {(n: HtmlNodeRef) => number} where a tag or attribute name ends (0 for a parser-inserted tag) */
49394934
const _nameEndOf = (n) => (n < 0 ? _attrNameEnds[-n] : _nodeNameEnds[n]);
4940-
/** @type {(a: HtmlAttributeRef) => number} where an attribute's value starts (-1 without one) */
4941-
const _valueStartOf = (a) => (a < 0 ? _attrValueStarts[-a] : -1);
4942-
/** @type {(a: HtmlAttributeRef) => number} where an attribute's value ends (-1 without one) */
4943-
const _valueEndOf = (a) =>
4944-
a < 0 && _attrValueStarts[-a] !== -1 ? _attrValueEnds[-a] : -1;
49454935
/** @type {(n: HtmlNodeRef) => number} where an element's opening tag ends */
49464936
const _contentStartOf = (n) => _nodeTagEnds[n];
4947-
/** @type {(n: HtmlNodeRef) => number} under `skip.text`, where a raw-text element's body ends (`tagEnd` when empty) */
4948-
const _contentEndOf = (n) =>
4949-
(_S._nodeFlags[n] & FLAG_HAS_TEMPLATE) !== 0
4950-
? _nodeTagEnds[n]
4951-
: _nodeContentEnds[n];
49524937

49534938
/** @type {(n: HtmlNodeRef) => number} how many attributes an element has */
49544939
const _attributeCountOf = (n) => _nodeAttrCounts[n];
@@ -4960,9 +4945,6 @@ const _attributeAt = (i, n) => _nodeAttrStarts[n] + i;
49604945
const _findAttribute = (name, n) =>
49614946
_findAttr(_nodeAttrStarts[n], _nodeAttrCounts[n], name);
49624947

4963-
/** @type {(a: number) => string} an attribute's value as written, by its path id */
4964-
const _attrRawValue = (a) => _attrValueOf(-a);
4965-
49664948
/** @type {(i: number) => string} an attribute's name */
49674949
const _attrNameOf = (i) => _attrNames[i];
49684950

@@ -5579,7 +5561,7 @@ const internLowerName = (table, input, start, end) => {
55795561
table.noted[memoSlot] = 1;
55805562
table.sourceBacked.push(memoSlot);
55815563
}
5582-
return (memo[memoSlot] = _asciiLowerCase(input.slice(start, end)));
5564+
return (memo[memoSlot] = asciiLowercase(input.slice(start, end)));
55835565
};
55845566

55855567
// Every tag name the tree builder compares against (the sets above already
@@ -8389,7 +8371,7 @@ const handleDoctypeToken = (
83898371
tok.name =
83908372
nameStart === -1
83918373
? ""
8392-
: _asciiLowerCase(doctypePart(input, nameStart, nameEnd));
8374+
: asciiLowercase(doctypePart(input, nameStart, nameEnd));
83938375
tok.publicId =
83948376
publicStart === -1 ? null : doctypePart(input, publicStart, publicEnd);
83958377
tok.systemId =
@@ -9977,14 +9959,12 @@ const _minifyStyleAttribute = (raw) => {
99779959
};
99789960

99799961
/**
9980-
* Trim ASCII whitespace, which is all these grammars skip — `String#trim` also
9981-
* eats NBSP and the other Unicode spaces, which they read as ordinary
9982-
* characters (a leading NBSP makes an integer unparsable, and is a class token
9983-
* of its own).
9962+
* Infra's strip leading and trailing ASCII whitespace: `String#trim` also eats
9963+
* NBSP and the other Unicode spaces, which HTML's grammars read as characters.
99849964
* @param {string} value the value
99859965
* @returns {string} the trimmed value
99869966
*/
9987-
const _asciiTrim = (value) => {
9967+
const stripAsciiWhitespace = (value) => {
99889968
let start = 0;
99899969
let end = value.length;
99909970
while (start < end && isAsciiWhitespace(value.charCodeAt(start))) start++;
@@ -10000,7 +9980,7 @@ const _asciiTrim = (value) => {
100009980
* @returns {string} the normalized list
100019981
*/
100029982
const _normalizeViewport = (raw) =>
10003-
_asciiTrim(raw)
9983+
stripAsciiWhitespace(raw)
100049984
.replace(/[\t\n\f\r ]*([,;=])[\t\n\f\r ]*/g, "$1")
100059985
.replace(/[\t\n\f\r ]+/g, " ");
100069986

@@ -10055,8 +10035,10 @@ const _minimalAttributeValue = (value) => {
1005510035
const _isRedundantAttribute = (markers, defaults, tagName, name, value) => {
1005610036
// Looked up by what the value says, not how it was spelled — guarded on the
1005710037
// `&` so a value carrying no reference pays nothing.
10058-
const lowered = _asciiLowerCase(
10059-
_asciiTrim(value.includes("&") ? decodeEntities(value, true) : value)
10038+
const lowered = asciiLowercase(
10039+
stripAsciiWhitespace(
10040+
value.includes("&") ? decodeEntities(value, true) : value
10041+
)
1006010042
);
1006110043
// `<script type>`: only a spelling that names JavaScript, which is what the
1006210044
// element already runs. `module` and the empty spelling say something.
@@ -10093,9 +10075,11 @@ const _isRemovableEmptyAttribute = (elementName, name, value) => {
1009310075
if (on === undefined || (on !== null && !on.has(elementName))) return false;
1009410076
// Read decoded: a reference is how a value is spelled, not what it says, and
1009510077
// the tokenizer hands the element the same empty either way. Guarded on the
10096-
// `&`, and `_asciiTrim` returns the input when there is nothing to trim.
10078+
// `&`, and `stripAsciiWhitespace` returns the input when there is nothing to trim.
1009710079
return (
10098-
_asciiTrim(value.includes("&") ? decodeEntities(value, true) : value) === ""
10080+
stripAsciiWhitespace(
10081+
value.includes("&") ? decodeEntities(value, true) : value
10082+
) === ""
1009910083
);
1010010084
};
1010110085

@@ -10137,8 +10121,8 @@ const _isCssStyleElement = (path, element) => {
1013710121
// Read the way `_scriptType` reads its own: the value the spec matches is
1013810122
// the one the parser read, so a reference is decoded first.
1013910123
const raw = attribute.value;
10140-
const value = _asciiLowerCase(
10141-
_asciiTrim(raw.includes("&") ? decodeEntities(raw, true) : raw)
10124+
const value = asciiLowercase(
10125+
stripAsciiWhitespace(raw.includes("&") ? decodeEntities(raw, true) : raw)
1014210126
);
1014310127
return value === "" || value === "text/css";
1014410128
}
@@ -10704,7 +10688,7 @@ const _attributeDataUrl = (tagName, name, value) => {
1070410688
if (elements === undefined || (elements !== null && !elements.has(tagName))) {
1070510689
return null;
1070610690
}
10707-
const url = _asciiTrim(value);
10691+
const url = stripAsciiWhitespace(value);
1070810692
// The URL parser drops every tab and newline, so a payload holding one is
1070910693
// not the text the browser reads.
1071010694
if ((url.charCodeAt(0) | 0x20) !== 0x64 || /[\t\n\r]/.test(url)) return null;
@@ -10835,7 +10819,9 @@ const _normalizeTokenList = (raw, isSet, sort) => {
1083510819
}
1083610820
if (!dirty && tokenStart === -1 && length !== 0) dirty = true;
1083710821
if (!dirty && count === 0) return raw;
10838-
const collapsed = dirty ? _asciiTrim(raw).replace(/[\t\n\f\r ]+/g, " ") : raw;
10822+
const collapsed = dirty
10823+
? stripAsciiWhitespace(raw).replace(/[\t\n\f\r ]+/g, " ")
10824+
: raw;
1083910825
if (dirty && !collapsed.includes(" ")) return collapsed;
1084010826
if (!isSet) return collapsed;
1084110827
// Cutting the list into tokens is the allocation this runs often enough to
@@ -10941,7 +10927,7 @@ const _sortedTokenList = (collapsed) => {
1094110927
const _normalizeCommaList = (raw) =>
1094210928
raw
1094310929
.split(",")
10944-
.map((item) => _asciiTrim(item))
10930+
.map((item) => stripAsciiWhitespace(item))
1094510931
.join(",");
1094610932

1094710933
/**
@@ -10955,7 +10941,7 @@ const _normalizeCommaList = (raw) =>
1095510941
* @returns {string} the normalized integer
1095610942
*/
1095710943
const _normalizeInteger = (raw, signed) => {
10958-
const value = _asciiTrim(raw);
10944+
const value = stripAsciiWhitespace(raw);
1095910945
if (!(signed ? /^[+-]?\d+$/ : /^\d+$/).test(value)) return raw;
1096010946
const negative = value.charCodeAt(0) === 0x2d;
1096110947
const digits = value.replace(/^[+-]/, "").replace(/^0+(?=\d)/, "");
@@ -10993,7 +10979,7 @@ const _rewriteAttributeValue = (element, name, raw, viewport, enumeratedOn) => {
1099310979
// Case-insensitively, and nothing else: an enumerated attribute names a
1099410980
// keyword, so `type="time "` is not `time` — it is the value no keyword
1099510981
// matches, which puts the input in its default state.
10996-
const lowered = _asciiLowerCase(raw);
10982+
const lowered = asciiLowercase(raw);
1099710983
// A value the spec does not enumerate is left exactly as written:
1099810984
// `target` names a browsing context, and `type` on a custom element
1099910985
// means what it says.
@@ -11029,7 +11015,7 @@ const _rewriteAttributeValue = (element, name, raw, viewport, enumeratedOn) => {
1102911015
? URL_ATTRIBUTES.get(name)
1103011016
: undefined;
1103111017
if (urlOn !== undefined && (urlOn === null || urlOn.has(element))) {
11032-
return _asciiTrim(raw);
11018+
return stripAsciiWhitespace(raw);
1103311019
}
1103411020
const integerOn = _transforms.normalizeNumericAttributes
1103511021
? INTEGER_ATTRIBUTES.get(name)
@@ -11054,8 +11040,10 @@ const _isViewportMeta = (path) => {
1105411040
if (attribute.name === "name") {
1105511041
const value = attribute.value;
1105611042
return (
11057-
_asciiLowerCase(
11058-
_asciiTrim(value.includes("&") ? decodeEntities(value, true) : value)
11043+
asciiLowercase(
11044+
stripAsciiWhitespace(
11045+
value.includes("&") ? decodeEntities(value, true) : value
11046+
)
1105911047
) === "viewport"
1106011048
);
1106111049
}
@@ -11088,7 +11076,7 @@ const _normalizeSrcset = (raw) => {
1108811076
let descriptor = raw.slice(end, nextStart);
1108911077
const comma = descriptor.indexOf(",");
1109011078
if (comma !== -1) descriptor = descriptor.slice(0, comma);
11091-
descriptor = _asciiTrim(descriptor).replace(/[\t\n\f\r ]+/g, " ");
11079+
descriptor = stripAsciiWhitespace(descriptor).replace(/[\t\n\f\r ]+/g, " ");
1109211080
// A descriptor-less URL runs to the next whitespace, so without one it
1109311081
// would swallow the separating comma and the URL behind it.
1109411082
if (i !== 0) out += previousBare ? ", " : ",";
@@ -11099,13 +11087,13 @@ const _normalizeSrcset = (raw) => {
1109911087
};
1110011088

1110111089
/**
11102-
* ASCII-lowercase a name or a keyword value, which is all HTML folds — a Unicode
11103-
* `toLowerCase` maps characters the parser leaves alone (U+212A onto `k`).
11104-
* Returns the input untouched when there is nothing to fold (the common case).
11090+
* Infra's ASCII lowercase, which is all HTML folds: `toLowerCase` also maps
11091+
* characters the parser leaves alone (U+212A onto `k`). Returns the input
11092+
* itself when there is nothing to fold.
1110511093
* @param {string} value source name or value
1110611094
* @returns {string} the value as the parser would match it
1110711095
*/
11108-
const _asciiLowerCase = (value) => {
11096+
const asciiLowercase = (value) => {
1110911097
for (let i = 0; i < value.length; i++) {
1111011098
const c = value.charCodeAt(i);
1111111099
if (c >= 0x41 && c <= 0x5a) {
@@ -11410,7 +11398,7 @@ const _minifyOpenTag = (path, open) => {
1141011398
if (!folds) return open;
1141111399
}
1141211400
const sourceName = open.slice(1, nameEnd);
11413-
const elementName = html ? _asciiLowerCase(sourceName) : sourceName;
11401+
const elementName = html ? asciiLowercase(sourceName) : sourceName;
1141411402
let out = `<${elementName}`;
1141511403
const isViewportMeta =
1141611404
html && _nodeStrings[node] === "meta" && _isViewportMeta(path);
@@ -11638,7 +11626,7 @@ const _minifyOpenTag = (path, open) => {
1163811626
_transforms.collapseBooleanAttributes &&
1163911627
(_transforms.collapseBooleanAttributes === "all" ||
1164011628
(presence.length === attributeName.length &&
11641-
_asciiLowerCase(presence) === attributeName)) &&
11629+
asciiLowercase(presence) === attributeName)) &&
1164211630
_isBooleanAttribute(elementName, attributeName)))
1164311631
) {
1164411632
out += ` ${attributeName}`;
@@ -11685,7 +11673,7 @@ const _minifyOpenTag = (path, open) => {
1168511673
: rawValue
1168611674
: null;
1168711675

11688-
if (handler !== null && _asciiTrim(handler) !== "") {
11676+
if (handler !== null && stripAsciiWhitespace(handler) !== "") {
1168911677
const delimiter = sourceDelimiter;
1169011678
// A foreign `/>` reads back whether this value ends unquoted, which a
1169111679
// deferred answer is not there to decide: the source's quoting stands.
@@ -12086,7 +12074,7 @@ const _elementCloseTag = (path, name, open, innerEmpty, minify) => {
1208612074
) {
1208712075
return `</${name}>`;
1208812076
}
12089-
return `</${_asciiLowerCase(_closeTagOf(path.node).slice(2, -1)).trim() || name}>`;
12077+
return `</${asciiLowercase(_closeTagOf(path.node).slice(2, -1)).trim() || name}>`;
1209012078
}
1209112079
const sourceClose = _closeTagOf(path.node);
1209212080
return sourceClose === "" ? `</${name}>` : sourceClose;
@@ -12392,7 +12380,7 @@ const _isHiddenInput = (path, node) => {
1239212380
for (let i = 0, n = _attributeCountOf(node); i < n; i++) {
1239312381
const a = _attributeAt(i, node);
1239412382
if (_attrNameOf(a) === "type") {
12395-
return _asciiLowerCase(_attrValueOf(a)) === "hidden";
12383+
return asciiLowercase(_attrValueOf(a)) === "hidden";
1239612384
}
1239712385
}
1239812386
return false;
@@ -12590,7 +12578,7 @@ const _digest = (root) => {
1259012578
let pendingText = "";
1259112579
let pendingDepth = 0;
1259212580
const flushText = () => {
12593-
const text = _asciiTrim(pendingText);
12581+
const text = stripAsciiWhitespace(pendingText);
1259412582
pendingText = "";
1259512583
if (text !== "") out.push(`${pendingDepth}\u0001t\u0001${text}`);
1259612584
};
@@ -12672,7 +12660,7 @@ const _digest = (root) => {
1267212660
html &&
1267312661
a.value.length === a.name.length &&
1267412662
_isBooleanAttribute(elementName, a.name) &&
12675-
_asciiLowerCase(a.value) === a.name
12663+
asciiLowercase(a.value) === a.name
1267612664
? ""
1267712665
: decodeEntities(a.value, true)
1267812666
}`
@@ -14379,25 +14367,16 @@ module.exports.QUOTE_NONE = QUOTE_NONE;
1437914367
module.exports.QUOTE_SINGLE = QUOTE_SINGLE;
1438014368
module.exports.SVG_TYPE = SVG_TYPE;
1438114369
module.exports._S = _S;
14382-
module.exports._attrRawValue = _attrRawValue;
1438314370
module.exports._attributeCountOf = _attributeCountOf;
1438414371
module.exports._attributeList = _attributeList;
14385-
module.exports._contentEndOf = _contentEndOf;
14386-
module.exports._contentStartOf = _contentStartOf;
1438714372
module.exports._dataOf = _dataOf;
14388-
module.exports._endOf = _endOf;
14389-
module.exports._nameEndOf = _nameEndOf;
14390-
module.exports._nameStartOf = _nameStartOf;
14391-
module.exports._startOf = _startOf;
14392-
module.exports._valueEndOf = _valueEndOf;
14393-
module.exports._valueStartOf = _valueStartOf;
1439414373
module.exports._firstChild = _firstChild;
1439514374
module.exports._tagNameOf = _tagNameOf;
1439614375
module.exports._namespaceOf = _namespaceOf;
1439714376
module.exports._nextSibling = _nextSibling;
1439814377
module.exports._parentOf = _parentOf;
14399-
module.exports._asciiLowerCase = _asciiLowerCase;
14400-
module.exports._asciiTrim = _asciiTrim;
14378+
module.exports.asciiLowercase = asciiLowercase;
14379+
module.exports.stripAsciiWhitespace = stripAsciiWhitespace;
1440114380
module.exports._attributePrints = _attributePrints;
1440214381
module.exports._canOmitEndTag = _canOmitEndTag;
1440314382
module.exports._canOmitStartTag = _canOmitStartTag;
@@ -14430,8 +14409,6 @@ module.exports._synthesizeOpenTag = _synthesizeOpenTag;
1443014409
module.exports._takeRenderedSheet = _takeRenderedSheet;
1443114410
module.exports._rendersNow = _rendersNow;
1443214411
module.exports._sheetDecides = _sheetDecides;
14433-
module.exports._SHEET_LEADING_AT_RULE_REGEXP = _SHEET_LEADING_AT_RULE_REGEXP;
14434-
module.exports._mergeableStyleAfter = _mergeableStyleAfter;
1443514412
module.exports._onlyTextChild = _onlyTextChild;
1443614413
module.exports._sheetClosing = _sheetClosing;
1443714414
module.exports._styleRunText = _styleRunText;

0 commit comments

Comments
 (0)