parse.js 109 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332
  1. //.CommonJS
  2. var CSSOM = {};
  3. var regexPatterns = require("./regexPatterns").regexPatterns;
  4. ///CommonJS
  5. /**
  6. * Parses a CSS string and returns a `CSSStyleSheet` object representing the parsed stylesheet.
  7. *
  8. * @param {string} token - The CSS string to parse.
  9. * @param {object} [opts] - Optional parsing options.
  10. * @param {object} [opts.globalObject] - An optional global object to prioritize over the window object. Useful on jsdom webplatform tests.
  11. * @param {Element | ProcessingInstruction} [opts.ownerNode] - The owner node of the stylesheet.
  12. * @param {CSSRule} [opts.ownerRule] - The owner rule of the stylesheet.
  13. * @param {CSSOM.CSSStyleSheet} [opts.styleSheet] - Reuse a style sheet instead of creating a new one (e.g. as `parentStyleSheet`)
  14. * @param {CSSOM.CSSRuleList} [opts.cssRules] - Prepare all rules in this list instead of mutating the style sheet continually
  15. * @param {function|boolean} [errorHandler] - Optional error handler function or `true` to use `console.error`.
  16. * @returns {CSSOM.CSSStyleSheet} The parsed `CSSStyleSheet` object.
  17. */
  18. CSSOM.parse = function parse(token, opts, errorHandler) {
  19. errorHandler = errorHandler === true ? (console && console.error) : errorHandler;
  20. var i = 0;
  21. /**
  22. "before-selector" or
  23. "selector" or
  24. "atRule" or
  25. "atBlock" or
  26. "conditionBlock" or
  27. "before-name" or
  28. "name" or
  29. "before-value" or
  30. "value"
  31. */
  32. var state = "before-selector";
  33. var index;
  34. var buffer = "";
  35. var valueParenthesisDepth = 0;
  36. var hasUnmatchedQuoteInSelector = false; // Track if current selector has unmatched quote
  37. var SIGNIFICANT_WHITESPACE = {
  38. "name": true,
  39. "before-name": true,
  40. "selector": true,
  41. "value": true,
  42. "value-parenthesis": true,
  43. "atRule": true,
  44. "importRule-begin": true,
  45. "importRule": true,
  46. "namespaceRule-begin": true,
  47. "namespaceRule": true,
  48. "atBlock": true,
  49. "containerBlock": true,
  50. "conditionBlock": true,
  51. "counterStyleBlock": true,
  52. "propertyBlock": true,
  53. 'documentRule-begin': true,
  54. "scopeBlock": true,
  55. "layerBlock": true,
  56. "pageBlock": true
  57. };
  58. var styleSheet;
  59. if (opts && opts.styleSheet) {
  60. styleSheet = opts.styleSheet;
  61. } else {
  62. if (opts && opts.globalObject && opts.globalObject.CSSStyleSheet) {
  63. styleSheet = new opts.globalObject.CSSStyleSheet();
  64. } else {
  65. styleSheet = new CSSOM.CSSStyleSheet();
  66. }
  67. styleSheet.__constructed = false;
  68. }
  69. var topScope;
  70. if (opts && opts.cssRules) {
  71. topScope = { cssRules: opts.cssRules };
  72. } else {
  73. topScope = styleSheet;
  74. }
  75. if (opts && opts.ownerNode) {
  76. styleSheet.__ownerNode = opts.ownerNode;
  77. var ownerNodeMedia = opts.ownerNode.media || (opts.ownerNode.getAttribute && opts.ownerNode.getAttribute("media"));
  78. if (ownerNodeMedia) {
  79. styleSheet.media.mediaText = ownerNodeMedia;
  80. }
  81. var ownerNodeTitle = opts.ownerNode.title || (opts.ownerNode.getAttribute && opts.ownerNode.getAttribute("title"));
  82. if (ownerNodeTitle) {
  83. styleSheet.__title = ownerNodeTitle;
  84. }
  85. }
  86. if (opts && opts.ownerRule) {
  87. styleSheet.__ownerRule = opts.ownerRule;
  88. }
  89. // @type CSSStyleSheet|CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSFontFaceRule|CSSKeyframesRule|CSSDocumentRule
  90. var currentScope = topScope;
  91. // @type CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSKeyframesRule|CSSDocumentRule
  92. var parentRule;
  93. var ancestorRules = [];
  94. var prevScope;
  95. var name, priority = "", styleRule, mediaRule, containerRule, counterStyleRule, propertyRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, scopeRule, pageRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
  96. // Track defined namespace prefixes for validation
  97. var definedNamespacePrefixes = {};
  98. // Track which rules have been added
  99. var ruleIdCounter = 0;
  100. var addedToParent = {};
  101. var addedToTopScope = {};
  102. var addedToCurrentScope = {};
  103. // Helper to get unique ID for tracking rules
  104. function getRuleId(rule) {
  105. if (!rule.__parseId) {
  106. rule.__parseId = ++ruleIdCounter;
  107. }
  108. return rule.__parseId;
  109. }
  110. // Cache last validation boundary position
  111. // to avoid rescanning the entire token string for each at-rule
  112. var lastValidationBoundary = 0;
  113. // Pre-compile validation regexes for common at-rules
  114. var validationRegexCache = {};
  115. function getValidationRegex(atRuleKey) {
  116. if (!validationRegexCache[atRuleKey]) {
  117. var sourceRuleRegExp = atRuleKey === "@import" ? forwardImportRuleValidationRegExp : forwardRuleValidationRegExp;
  118. validationRegexCache[atRuleKey] = new RegExp(atRuleKey + sourceRuleRegExp.source, sourceRuleRegExp.flags);
  119. }
  120. return validationRegexCache[atRuleKey];
  121. }
  122. // Import regex patterns from shared module
  123. var atKeyframesRegExp = regexPatterns.atKeyframesRegExp;
  124. var beforeRulePortionRegExp = regexPatterns.beforeRulePortionRegExp;
  125. var beforeRuleValidationRegExp = regexPatterns.beforeRuleValidationRegExp;
  126. var forwardRuleValidationRegExp = regexPatterns.forwardRuleValidationRegExp;
  127. var forwardImportRuleValidationRegExp = regexPatterns.forwardImportRuleValidationRegExp;
  128. // Pre-compile regexBefore to avoid creating it on every validateAtRule call
  129. var regexBefore = new RegExp(beforeRulePortionRegExp.source, beforeRulePortionRegExp.flags);
  130. var forwardRuleClosingBraceRegExp = regexPatterns.forwardRuleClosingBraceRegExp;
  131. var forwardRuleSemicolonAndOpeningBraceRegExp = regexPatterns.forwardRuleSemicolonAndOpeningBraceRegExp;
  132. var cssCustomIdentifierRegExp = regexPatterns.cssCustomIdentifierRegExp;
  133. var startsWithCombinatorRegExp = regexPatterns.startsWithCombinatorRegExp;
  134. var atPageRuleSelectorRegExp = regexPatterns.atPageRuleSelectorRegExp;
  135. var startsWithHexEscapeRegExp = regexPatterns.startsWithHexEscapeRegExp;
  136. var identStartCharRegExp = regexPatterns.identStartCharRegExp;
  137. var identCharRegExp = regexPatterns.identCharRegExp;
  138. var specialCharsNeedEscapeRegExp = regexPatterns.specialCharsNeedEscapeRegExp;
  139. var combinatorOrSeparatorRegExp = regexPatterns.combinatorOrSeparatorRegExp;
  140. var afterHexEscapeSeparatorRegExp = regexPatterns.afterHexEscapeSeparatorRegExp;
  141. var trailingSpaceSeparatorRegExp = regexPatterns.trailingSpaceSeparatorRegExp;
  142. var endsWithHexEscapeRegExp = regexPatterns.endsWithHexEscapeRegExp;
  143. var attributeSelectorContentRegExp = regexPatterns.attributeSelectorContentRegExp;
  144. var pseudoElementRegExp = regexPatterns.pseudoElementRegExp;
  145. var invalidCombinatorLtGtRegExp = regexPatterns.invalidCombinatorLtGtRegExp;
  146. var invalidCombinatorDoubleGtRegExp = regexPatterns.invalidCombinatorDoubleGtRegExp;
  147. var consecutiveCombinatorsRegExp = regexPatterns.consecutiveCombinatorsRegExp;
  148. var invalidSlottedRegExp = regexPatterns.invalidSlottedRegExp;
  149. var invalidPartRegExp = regexPatterns.invalidPartRegExp;
  150. var invalidCueRegExp = regexPatterns.invalidCueRegExp;
  151. var invalidCueRegionRegExp = regexPatterns.invalidCueRegionRegExp;
  152. var invalidNestingPattern = regexPatterns.invalidNestingPattern;
  153. var emptyPseudoClassRegExp = regexPatterns.emptyPseudoClassRegExp;
  154. var whitespaceNormalizationRegExp = regexPatterns.whitespaceNormalizationRegExp;
  155. var newlineRemovalRegExp = regexPatterns.newlineRemovalRegExp;
  156. var whitespaceAndDotRegExp = regexPatterns.whitespaceAndDotRegExp;
  157. var declarationOrOpenBraceRegExp = regexPatterns.declarationOrOpenBraceRegExp;
  158. var ampersandRegExp = regexPatterns.ampersandRegExp;
  159. var hexEscapeSequenceRegExp = regexPatterns.hexEscapeSequenceRegExp;
  160. var attributeCaseFlagRegExp = regexPatterns.attributeCaseFlagRegExp;
  161. var prependedAmpersandRegExp = regexPatterns.prependedAmpersandRegExp;
  162. var openBraceGlobalRegExp = regexPatterns.openBraceGlobalRegExp;
  163. var closeBraceGlobalRegExp = regexPatterns.closeBraceGlobalRegExp;
  164. var scopePreludeSplitRegExp = regexPatterns.scopePreludeSplitRegExp;
  165. var leadingWhitespaceRegExp = regexPatterns.leadingWhitespaceRegExp;
  166. var doubleQuoteRegExp = regexPatterns.doubleQuoteRegExp;
  167. var backslashRegExp = regexPatterns.backslashRegExp;
  168. /**
  169. * Searches for the first occurrence of a CSS at-rule statement terminator (`;` or `}`)
  170. * that is not inside a brace block within the given string. Mimics the behavior of a
  171. * regular expression match for such terminators, including any trailing whitespace.
  172. * @param {string} str - The string to search for at-rule statement terminators.
  173. * @returns {object | null} {0: string, index: number} or null if no match is found.
  174. */
  175. function atRulesStatemenRegExpES5Alternative(ruleSlice) {
  176. for (var i = 0; i < ruleSlice.length; i++) {
  177. var char = ruleSlice[i];
  178. if (char === ';' || char === '}') {
  179. // Simulate negative lookbehind: check if there is a { before this position
  180. var sliceBefore = ruleSlice.substring(0, i);
  181. var openBraceIndex = sliceBefore.indexOf('{');
  182. if (openBraceIndex === -1) {
  183. // No { found before, so we treat it as a valid match
  184. var match = char;
  185. var j = i + 1;
  186. while (j < ruleSlice.length && /\s/.test(ruleSlice[j])) {
  187. match += ruleSlice[j];
  188. j++;
  189. }
  190. var matchObj = [match];
  191. matchObj.index = i;
  192. matchObj.input = ruleSlice;
  193. return matchObj;
  194. }
  195. }
  196. }
  197. return null;
  198. }
  199. /**
  200. * Finds the first balanced block (including nested braces) in the string, starting from fromIndex.
  201. * Returns an object similar to RegExp.prototype.match output.
  202. * @param {string} str - The string to search.
  203. * @param {number} [fromIndex=0] - The index to start searching from.
  204. * @returns {object|null} - { 0: matchedString, index: startIndex, input: str } or null if not found.
  205. */
  206. function matchBalancedBlock(str, fromIndex) {
  207. fromIndex = fromIndex || 0;
  208. var openIndex = str.indexOf('{', fromIndex);
  209. if (openIndex === -1) return null;
  210. var depth = 0;
  211. for (var i = openIndex; i < str.length; i++) {
  212. if (str[i] === '{') {
  213. depth++;
  214. } else if (str[i] === '}') {
  215. depth--;
  216. if (depth === 0) {
  217. var matchedString = str.slice(openIndex, i + 1);
  218. return {
  219. 0: matchedString,
  220. index: openIndex,
  221. input: str
  222. };
  223. }
  224. }
  225. }
  226. return null;
  227. }
  228. /**
  229. * Advances the index `i` to skip over a balanced block of curly braces in the given string.
  230. * This is typically used to ignore the contents of a CSS rule block.
  231. *
  232. * @param {number} i - The current index in the string to start searching from.
  233. * @param {string} str - The string containing the CSS code.
  234. * @param {number} fromIndex - The index in the string where the balanced block search should begin.
  235. * @returns {number} The updated index after skipping the balanced block.
  236. */
  237. function ignoreBalancedBlock(i, str, fromIndex) {
  238. var ruleClosingMatch = matchBalancedBlock(str, fromIndex);
  239. if (ruleClosingMatch) {
  240. var ignoreRange = ruleClosingMatch.index + ruleClosingMatch[0].length;
  241. i += ignoreRange;
  242. if (token.charAt(i) === '}') {
  243. i -= 1;
  244. }
  245. } else {
  246. i += str.length;
  247. }
  248. return i;
  249. }
  250. /**
  251. * Parses the scope prelude and extracts start and end selectors.
  252. * @param {string} preludeContent - The scope prelude content (without @scope keyword)
  253. * @returns {object} Object with startSelector and endSelector properties
  254. */
  255. function parseScopePrelude(preludeContent) {
  256. var parts = preludeContent.split(scopePreludeSplitRegExp);
  257. // Restore the parentheses that were consumed by the split
  258. if (parts.length === 2) {
  259. parts[0] = parts[0] + ')';
  260. parts[1] = '(' + parts[1];
  261. }
  262. var hasStart = parts[0] &&
  263. parts[0].charAt(0) === '(' &&
  264. parts[0].charAt(parts[0].length - 1) === ')';
  265. var hasEnd = parts[1] &&
  266. parts[1].charAt(0) === '(' &&
  267. parts[1].charAt(parts[1].length - 1) === ')';
  268. // Handle case: @scope to (<end>)
  269. var hasOnlyEnd = !hasStart &&
  270. !hasEnd &&
  271. parts[0].indexOf('to (') === 0 &&
  272. parts[0].charAt(parts[0].length - 1) === ')';
  273. var startSelector = '';
  274. var endSelector = '';
  275. if (hasStart) {
  276. startSelector = parts[0].slice(1, -1).trim();
  277. }
  278. if (hasEnd) {
  279. endSelector = parts[1].slice(1, -1).trim();
  280. }
  281. if (hasOnlyEnd) {
  282. endSelector = parts[0].slice(4, -1).trim();
  283. }
  284. return {
  285. startSelector: startSelector,
  286. endSelector: endSelector,
  287. hasStart: hasStart,
  288. hasEnd: hasEnd,
  289. hasOnlyEnd: hasOnlyEnd
  290. };
  291. };
  292. /**
  293. * Checks if a selector contains pseudo-elements.
  294. * @param {string} selector - The CSS selector to check
  295. * @returns {boolean} True if the selector contains pseudo-elements
  296. */
  297. function hasPseudoElement(selector) {
  298. // Match only double-colon (::) pseudo-elements
  299. // Also match legacy single-colon pseudo-elements: :before, :after, :first-line, :first-letter
  300. // These must NOT be followed by alphanumeric characters (to avoid matching :before-x or similar)
  301. return pseudoElementRegExp.test(selector);
  302. };
  303. /**
  304. * Validates balanced parentheses, brackets, and quotes in a selector.
  305. *
  306. * @param {string} selector - The CSS selector to validate
  307. * @param {boolean} trackAttributes - Whether to track attribute selector context
  308. * @param {boolean} useStack - Whether to use a stack for parentheses (needed for nested validation)
  309. * @returns {boolean} True if the syntax is valid (all brackets, parentheses, and quotes are balanced)
  310. */
  311. function validateBalancedSyntax(selector, trackAttributes, useStack) {
  312. var parenDepth = 0;
  313. var bracketDepth = 0;
  314. var inSingleQuote = false;
  315. var inDoubleQuote = false;
  316. var inAttr = false;
  317. var stack = useStack ? [] : null;
  318. for (var i = 0; i < selector.length; i++) {
  319. var char = selector[i];
  320. // Handle escape sequences - skip hex escapes or simple escapes
  321. if (char === '\\') {
  322. var escapeLen = getEscapeSequenceLength(selector, i);
  323. if (escapeLen > 0) {
  324. i += escapeLen - 1; // -1 because loop will increment
  325. continue;
  326. }
  327. }
  328. if (inSingleQuote) {
  329. if (char === "'") {
  330. inSingleQuote = false;
  331. }
  332. } else if (inDoubleQuote) {
  333. if (char === '"') {
  334. inDoubleQuote = false;
  335. }
  336. } else if (trackAttributes && inAttr) {
  337. if (char === "]") {
  338. inAttr = false;
  339. } else if (char === "'") {
  340. inSingleQuote = true;
  341. } else if (char === '"') {
  342. inDoubleQuote = true;
  343. }
  344. } else {
  345. if (trackAttributes && char === "[") {
  346. inAttr = true;
  347. } else if (char === "'") {
  348. inSingleQuote = true;
  349. } else if (char === '"') {
  350. inDoubleQuote = true;
  351. } else if (char === '(') {
  352. if (useStack) {
  353. stack.push("(");
  354. } else {
  355. parenDepth++;
  356. }
  357. } else if (char === ')') {
  358. if (useStack) {
  359. if (!stack.length || stack.pop() !== "(") {
  360. return false;
  361. }
  362. } else {
  363. parenDepth--;
  364. if (parenDepth < 0) {
  365. return false;
  366. }
  367. }
  368. } else if (char === '[') {
  369. bracketDepth++;
  370. } else if (char === ']') {
  371. bracketDepth--;
  372. if (bracketDepth < 0) {
  373. return false;
  374. }
  375. }
  376. }
  377. }
  378. // Check if everything is balanced
  379. if (useStack) {
  380. return stack.length === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote && !inAttr;
  381. } else {
  382. return parenDepth === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote;
  383. }
  384. };
  385. /**
  386. * Checks for basic syntax errors in selectors (mismatched parentheses, brackets, quotes).
  387. * @param {string} selector - The CSS selector to check
  388. * @returns {boolean} True if there are syntax errors
  389. */
  390. function hasBasicSyntaxError(selector) {
  391. return !validateBalancedSyntax(selector, false, false);
  392. };
  393. /**
  394. * Checks for invalid combinator patterns in selectors.
  395. * @param {string} selector - The CSS selector to check
  396. * @returns {boolean} True if the selector contains invalid combinators
  397. */
  398. function hasInvalidCombinators(selector) {
  399. // Check for invalid combinator patterns:
  400. // - <> (not a valid combinator)
  401. // - >> (deep descendant combinator, deprecated and invalid)
  402. // - Multiple consecutive combinators like >>, >~, etc.
  403. if (invalidCombinatorLtGtRegExp.test(selector)) return true;
  404. if (invalidCombinatorDoubleGtRegExp.test(selector)) return true;
  405. // Check for other invalid consecutive combinator patterns
  406. if (consecutiveCombinatorsRegExp.test(selector)) return true;
  407. return false;
  408. };
  409. /**
  410. * Checks for invalid pseudo-like syntax (function calls without proper pseudo prefix).
  411. * @param {string} selector - The CSS selector to check
  412. * @returns {boolean} True if the selector contains invalid pseudo-like syntax
  413. */
  414. function hasInvalidPseudoSyntax(selector) {
  415. // Check for specific known pseudo-elements used without : or :: prefix
  416. // Examples: slotted(div), part(name), cue(selector)
  417. // These are ONLY valid as ::slotted(), ::part(), ::cue()
  418. var invalidPatterns = [
  419. invalidSlottedRegExp,
  420. invalidPartRegExp,
  421. invalidCueRegExp,
  422. invalidCueRegionRegExp
  423. ];
  424. for (var i = 0; i < invalidPatterns.length; i++) {
  425. if (invalidPatterns[i].test(selector)) {
  426. return true;
  427. }
  428. }
  429. return false;
  430. };
  431. /**
  432. * Checks for invalid nesting selector (&) usage.
  433. * The & selector cannot be directly followed by a type selector without a delimiter.
  434. * Valid: &.class, &#id, &[attr], &:hover, &::before, & div, &>div
  435. * Invalid: &div, &span
  436. * @param {string} selector - The CSS selector to check
  437. * @returns {boolean} True if the selector contains invalid & usage
  438. */
  439. function hasInvalidNestingSelector(selector) {
  440. // Check for & followed directly by a letter (type selector) without any delimiter
  441. // This regex matches & followed by a letter (start of type selector) that's not preceded by an escape
  442. // We need to exclude valid cases like &.class, &#id, &[attr], &:pseudo, &::pseudo, & (with space), &>
  443. return invalidNestingPattern.test(selector);
  444. };
  445. /**
  446. * Checks if an at-rule can be nested based on parent chain validation.
  447. * Used for at-rules like `@counter-style`, `@property` and `@font-face` rules that can only be nested inside
  448. * `CSSScopeRule` or `CSSConditionRule` without `CSSStyleRule` in parent chain.
  449. * @returns {boolean} `true` if nesting is allowed, `false` otherwise
  450. */
  451. function canAtRuleBeNested() {
  452. if (currentScope === topScope) {
  453. return true; // Top-level is always allowed
  454. }
  455. var hasStyleRuleInChain = false;
  456. var hasValidParent = false;
  457. // Check currentScope
  458. if (currentScope.constructor.name === 'CSSStyleRule') {
  459. hasStyleRuleInChain = true;
  460. } else if (currentScope instanceof CSSOM.CSSScopeRule || currentScope instanceof CSSOM.CSSConditionRule) {
  461. hasValidParent = true;
  462. }
  463. // Check ancestorRules for CSSStyleRule
  464. if (!hasStyleRuleInChain) {
  465. for (var j = 0; j < ancestorRules.length; j++) {
  466. if (ancestorRules[j].constructor.name === 'CSSStyleRule') {
  467. hasStyleRuleInChain = true;
  468. break;
  469. }
  470. if (ancestorRules[j] instanceof CSSOM.CSSScopeRule || ancestorRules[j] instanceof CSSOM.CSSConditionRule) {
  471. hasValidParent = true;
  472. }
  473. }
  474. }
  475. // Allow nesting if we have a valid parent and no style rule in the chain
  476. return hasValidParent && !hasStyleRuleInChain;
  477. }
  478. function validateAtRule(atRuleKey, validCallback, cannotBeNested) {
  479. var isValid = false;
  480. // Use cached regex instead of creating new one each time
  481. var ruleRegExp = getValidationRegex(atRuleKey);
  482. // Only slice what we need for validation (max 100 chars)
  483. // since we only check match at position 0
  484. var lookAheadLength = Math.min(100, token.length - i);
  485. var ruleSlice = token.slice(i, i + lookAheadLength);
  486. // Not all rules can be nested, if the rule cannot be nested and is in the root scope, do not perform the check
  487. var shouldPerformCheck = cannotBeNested && currentScope !== topScope ? false : true;
  488. // First, check if there is no invalid characters just after the at-rule
  489. if (shouldPerformCheck && ruleSlice.search(ruleRegExp) === 0) {
  490. // Only scan from the last known validation boundary
  491. var searchStart = Math.max(0, lastValidationBoundary);
  492. var beforeSlice = token.slice(searchStart, i);
  493. // Use pre-compiled regex instead of creating new one each time
  494. var matches = beforeSlice.match(regexBefore);
  495. var lastI = matches ? searchStart + beforeSlice.lastIndexOf(matches[matches.length - 1]) : searchStart;
  496. var toCheckSlice = token.slice(lastI, i);
  497. // Check if we don't have any invalid in the portion before the `at-rule` and the closest allowed character
  498. var checkedSlice = toCheckSlice.search(beforeRuleValidationRegExp);
  499. if (checkedSlice === 0) {
  500. isValid = true;
  501. // Update the validation boundary cache to this position
  502. lastValidationBoundary = lastI;
  503. }
  504. }
  505. // Additional validation for @scope rule
  506. if (isValid && atRuleKey === "@scope") {
  507. var openBraceIndex = ruleSlice.indexOf('{');
  508. if (openBraceIndex !== -1) {
  509. // Extract the rule prelude (everything between the at-rule and {)
  510. var rulePrelude = ruleSlice.slice(0, openBraceIndex).trim();
  511. // Skip past at-rule keyword and whitespace
  512. var preludeContent = rulePrelude.slice("@scope".length).trim();
  513. if (preludeContent.length > 0) {
  514. // Parse the scope prelude
  515. var parsedScopePrelude = parseScopePrelude(preludeContent);
  516. var startSelector = parsedScopePrelude.startSelector;
  517. var endSelector = parsedScopePrelude.endSelector;
  518. var hasStart = parsedScopePrelude.hasStart;
  519. var hasEnd = parsedScopePrelude.hasEnd;
  520. var hasOnlyEnd = parsedScopePrelude.hasOnlyEnd;
  521. // Validation rules for @scope:
  522. // 1. Empty selectors in parentheses are invalid: @scope () {} or @scope (.a) to () {}
  523. if ((hasStart && startSelector === '') || (hasEnd && endSelector === '') || (hasOnlyEnd && endSelector === '')) {
  524. isValid = false;
  525. }
  526. // 2. Pseudo-elements are invalid in scope selectors
  527. else if ((startSelector && hasPseudoElement(startSelector)) || (endSelector && hasPseudoElement(endSelector))) {
  528. isValid = false;
  529. }
  530. // 3. Basic syntax errors (mismatched parens, brackets, quotes)
  531. else if ((startSelector && hasBasicSyntaxError(startSelector)) || (endSelector && hasBasicSyntaxError(endSelector))) {
  532. isValid = false;
  533. }
  534. // 4. Invalid combinator patterns
  535. else if ((startSelector && hasInvalidCombinators(startSelector)) || (endSelector && hasInvalidCombinators(endSelector))) {
  536. isValid = false;
  537. }
  538. // 5. Invalid pseudo-like syntax (function without : or :: prefix)
  539. else if ((startSelector && hasInvalidPseudoSyntax(startSelector)) || (endSelector && hasInvalidPseudoSyntax(endSelector))) {
  540. isValid = false;
  541. }
  542. // 6. Invalid structure (no proper parentheses found when prelude is not empty)
  543. else if (!hasStart && !hasOnlyEnd) {
  544. isValid = false;
  545. }
  546. }
  547. // Empty prelude (@scope {}) is valid
  548. }
  549. }
  550. if (isValid && atRuleKey === "@page") {
  551. var openBraceIndex = ruleSlice.indexOf('{');
  552. if (openBraceIndex !== -1) {
  553. // Extract the rule prelude (everything between the at-rule and {)
  554. var rulePrelude = ruleSlice.slice(0, openBraceIndex).trim();
  555. // Skip past at-rule keyword and whitespace
  556. var preludeContent = rulePrelude.slice("@page".length).trim();
  557. if (preludeContent.length > 0) {
  558. var trimmedValue = preludeContent.trim();
  559. // Empty selector is valid for @page
  560. if (trimmedValue !== '') {
  561. // Parse @page selectorText for page name and pseudo-pages
  562. // Valid formats:
  563. // - (empty - no name, no pseudo-page)
  564. // - :left, :right, :first, :blank (pseudo-page only)
  565. // - named (named page only)
  566. // - named:first (named page with single pseudo-page)
  567. // - named:first:left (named page with multiple pseudo-pages)
  568. var match = trimmedValue.match(atPageRuleSelectorRegExp);
  569. if (match) {
  570. var pageName = match[1] || '';
  571. var pseudoPages = match[2] || '';
  572. // Validate page name if present
  573. if (pageName) {
  574. if (!cssCustomIdentifierRegExp.test(pageName)) {
  575. isValid = false;
  576. }
  577. }
  578. // Validate pseudo-pages if present
  579. if (pseudoPages) {
  580. var pseudos = pseudoPages.split(':').filter(function (p) { return p; });
  581. var validPseudos = ['left', 'right', 'first', 'blank'];
  582. var allValid = true;
  583. for (var j = 0; j < pseudos.length; j++) {
  584. if (validPseudos.indexOf(pseudos[j].toLowerCase()) === -1) {
  585. allValid = false;
  586. break;
  587. }
  588. }
  589. if (!allValid) {
  590. isValid = false;
  591. }
  592. }
  593. } else {
  594. isValid = false;
  595. }
  596. }
  597. }
  598. }
  599. }
  600. if (!isValid) {
  601. // If it's invalid the browser will simply ignore the entire invalid block
  602. // Use regex to find the closing brace of the invalid rule
  603. // Regex used above is not ES5 compliant. Using alternative.
  604. // var ruleStatementMatch = ruleSlice.match(atRulesStatemenRegExp); //
  605. var ruleStatementMatch = atRulesStatemenRegExpES5Alternative(ruleSlice);
  606. // If it's a statement inside a nested rule, ignore only the statement
  607. if (ruleStatementMatch && currentScope !== topScope) {
  608. var ignoreEnd = ruleStatementMatch[0].indexOf(";");
  609. i += ruleStatementMatch.index + ignoreEnd;
  610. return;
  611. }
  612. // Check if there's a semicolon before the invalid at-rule and the first opening brace
  613. if (atRuleKey === "@layer") {
  614. var ruleSemicolonAndOpeningBraceMatch = ruleSlice.match(forwardRuleSemicolonAndOpeningBraceRegExp);
  615. if (ruleSemicolonAndOpeningBraceMatch && ruleSemicolonAndOpeningBraceMatch[1] === ";") {
  616. // Ignore the rule block until the semicolon
  617. i += ruleSemicolonAndOpeningBraceMatch.index + ruleSemicolonAndOpeningBraceMatch[0].length;
  618. state = "before-selector";
  619. return;
  620. }
  621. }
  622. // Ignore the entire rule block (if it's a statement it should ignore the statement plus the next block)
  623. i = ignoreBalancedBlock(i, ruleSlice);
  624. state = "before-selector";
  625. } else {
  626. validCallback.call(this);
  627. }
  628. }
  629. // Helper functions for looseSelectorValidator
  630. // Defined outside to avoid recreation on every validation call
  631. /**
  632. * Check if character is a valid identifier start
  633. * @param {string} c - Character to check
  634. * @returns {boolean}
  635. */
  636. function isIdentStart(c) {
  637. return /[a-zA-Z_\u00A0-\uFFFF]/.test(c);
  638. }
  639. /**
  640. * Check if character is a valid identifier character
  641. * @param {string} c - Character to check
  642. * @returns {boolean}
  643. */
  644. function isIdentChar(c) {
  645. return /[a-zA-Z0-9_\u00A0-\uFFFF\-]/.test(c);
  646. }
  647. /**
  648. * Helper function to validate CSS selector syntax without regex backtracking.
  649. * Iteratively parses the selector string to identify valid components.
  650. *
  651. * Supports:
  652. * - Escaped characters (e.g., .class\!, #id\@name)
  653. * - Namespace selectors (ns|element, *|element, |element)
  654. * - All standard CSS selectors (class, ID, type, attribute, pseudo, etc.)
  655. * - Combinators (>, +, ~, whitespace)
  656. * - Nesting selector (&)
  657. *
  658. * This approach eliminates exponential backtracking by using explicit character-by-character
  659. * parsing instead of nested quantifiers in regex.
  660. *
  661. * @param {string} selector - The selector to validate
  662. * @returns {boolean} - True if valid selector syntax
  663. */
  664. function looseSelectorValidator(selector) {
  665. if (!selector || selector.length === 0) {
  666. return false;
  667. }
  668. var i = 0;
  669. var len = selector.length;
  670. var hasMatchedComponent = false;
  671. // Helper: Skip escaped character (backslash + hex escape or any char)
  672. function skipEscape() {
  673. if (i < len && selector[i] === '\\') {
  674. var escapeLen = getEscapeSequenceLength(selector, i);
  675. if (escapeLen > 0) {
  676. i += escapeLen; // Skip entire escape sequence
  677. return true;
  678. }
  679. }
  680. return false;
  681. }
  682. // Helper: Parse identifier (with possible escapes)
  683. function parseIdentifier() {
  684. var start = i;
  685. while (i < len) {
  686. if (skipEscape()) {
  687. continue;
  688. } else if (isIdentChar(selector[i])) {
  689. i++;
  690. } else {
  691. break;
  692. }
  693. }
  694. return i > start;
  695. }
  696. // Helper: Parse namespace prefix (optional)
  697. function parseNamespace() {
  698. var start = i;
  699. // Match: *| or identifier| or |
  700. if (i < len && selector[i] === '*') {
  701. i++;
  702. } else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\')) {
  703. parseIdentifier();
  704. }
  705. if (i < len && selector[i] === '|') {
  706. i++;
  707. return true;
  708. }
  709. // Rollback if no pipe found
  710. i = start;
  711. return false;
  712. }
  713. // Helper: Parse pseudo-class/element arguments (with balanced parens)
  714. function parsePseudoArgs() {
  715. if (i >= len || selector[i] !== '(') {
  716. return false;
  717. }
  718. i++; // Skip opening paren
  719. var depth = 1;
  720. var inString = false;
  721. var stringChar = '';
  722. while (i < len && depth > 0) {
  723. var c = selector[i];
  724. if (c === '\\' && i + 1 < len) {
  725. i += 2; // Skip escaped character
  726. } else if (!inString && (c === '"' || c === '\'')) {
  727. inString = true;
  728. stringChar = c;
  729. i++;
  730. } else if (inString && c === stringChar) {
  731. inString = false;
  732. i++;
  733. } else if (!inString && c === '(') {
  734. depth++;
  735. i++;
  736. } else if (!inString && c === ')') {
  737. depth--;
  738. i++;
  739. } else {
  740. i++;
  741. }
  742. }
  743. return depth === 0;
  744. }
  745. // Main parsing loop
  746. while (i < len) {
  747. var matched = false;
  748. var start = i;
  749. // Skip whitespace
  750. while (i < len && /\s/.test(selector[i])) {
  751. i++;
  752. }
  753. if (i > start) {
  754. hasMatchedComponent = true;
  755. continue;
  756. }
  757. // Match combinators: >, +, ~
  758. if (i < len && /[>+~]/.test(selector[i])) {
  759. i++;
  760. hasMatchedComponent = true;
  761. // Skip trailing whitespace
  762. while (i < len && /\s/.test(selector[i])) {
  763. i++;
  764. }
  765. continue;
  766. }
  767. // Match nesting selector: &
  768. if (i < len && selector[i] === '&') {
  769. i++;
  770. hasMatchedComponent = true;
  771. matched = true;
  772. }
  773. // Match class selector: .identifier
  774. else if (i < len && selector[i] === '.') {
  775. i++;
  776. if (parseIdentifier()) {
  777. hasMatchedComponent = true;
  778. matched = true;
  779. }
  780. }
  781. // Match ID selector: #identifier
  782. else if (i < len && selector[i] === '#') {
  783. i++;
  784. if (parseIdentifier()) {
  785. hasMatchedComponent = true;
  786. matched = true;
  787. }
  788. }
  789. // Match pseudo-class/element: :identifier or ::identifier
  790. else if (i < len && selector[i] === ':') {
  791. i++;
  792. if (i < len && selector[i] === ':') {
  793. i++; // Pseudo-element
  794. }
  795. if (parseIdentifier()) {
  796. parsePseudoArgs(); // Optional arguments
  797. hasMatchedComponent = true;
  798. matched = true;
  799. }
  800. }
  801. // Match attribute selector: [...]
  802. else if (i < len && selector[i] === '[') {
  803. i++;
  804. var depth = 1;
  805. while (i < len && depth > 0) {
  806. if (selector[i] === '\\') {
  807. i += 2;
  808. } else if (selector[i] === '\'') {
  809. i++;
  810. while (i < len && selector[i] !== '\'') {
  811. if (selector[i] === '\\') i += 2;
  812. else i++;
  813. }
  814. if (i < len) i++; // Skip closing quote
  815. } else if (selector[i] === '"') {
  816. i++;
  817. while (i < len && selector[i] !== '"') {
  818. if (selector[i] === '\\') i += 2;
  819. else i++;
  820. }
  821. if (i < len) i++; // Skip closing quote
  822. } else if (selector[i] === '[') {
  823. depth++;
  824. i++;
  825. } else if (selector[i] === ']') {
  826. depth--;
  827. i++;
  828. } else {
  829. i++;
  830. }
  831. }
  832. if (depth === 0) {
  833. hasMatchedComponent = true;
  834. matched = true;
  835. }
  836. }
  837. // Match type selector with optional namespace: [namespace|]identifier
  838. else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\' || selector[i] === '*' || selector[i] === '|')) {
  839. parseNamespace(); // Optional namespace prefix
  840. if (i < len && selector[i] === '*') {
  841. i++; // Universal selector
  842. hasMatchedComponent = true;
  843. matched = true;
  844. } else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\')) {
  845. if (parseIdentifier()) {
  846. hasMatchedComponent = true;
  847. matched = true;
  848. }
  849. }
  850. }
  851. // If no match found, invalid selector
  852. if (!matched && i === start) {
  853. return false;
  854. }
  855. }
  856. return hasMatchedComponent;
  857. }
  858. /**
  859. * Validates a basic CSS selector, allowing for deeply nested balanced parentheses in pseudo-classes.
  860. * This function replaces the previous basicSelectorRegExp.
  861. *
  862. * This function matches:
  863. * - Type selectors (e.g., `div`, `span`)
  864. * - Universal selector (`*`)
  865. * - Namespace selectors (e.g., `*|div`, `custom|div`, `|div`)
  866. * - ID selectors (e.g., `#header`, `#a\ b`, `#åèiöú`)
  867. * - Class selectors (e.g., `.container`, `.a\ b`, `.åèiöú`)
  868. * - Attribute selectors (e.g., `[type="text"]`)
  869. * - Pseudo-classes and pseudo-elements (e.g., `:hover`, `::before`, `:nth-child(2)`)
  870. * - Pseudo-classes with nested parentheses, including cases where parentheses are nested inside arguments,
  871. * such as `:has(.sel:nth-child(3n))`
  872. * - The parent selector (`&`)
  873. * - Combinators (`>`, `+`, `~`) with optional whitespace
  874. * - Whitespace (descendant combinator)
  875. *
  876. * Unicode and escape sequences are allowed in identifiers.
  877. *
  878. * @param {string} selector
  879. * @returns {boolean}
  880. */
  881. function basicSelectorValidator(selector) {
  882. // Guard against extremely long selectors to prevent potential regex performance issues
  883. // Reasonable selectors are typically under 1000 characters
  884. if (selector.length > 10000) {
  885. return false;
  886. }
  887. // Validate balanced syntax with attribute tracking and stack-based parentheses matching
  888. if (!validateBalancedSyntax(selector, true, true)) {
  889. return false;
  890. }
  891. // Check for invalid combinator patterns
  892. if (hasInvalidCombinators(selector)) {
  893. return false;
  894. }
  895. // Check for invalid pseudo-like syntax
  896. if (hasInvalidPseudoSyntax(selector)) {
  897. return false;
  898. }
  899. // Check for invalid nesting selector (&) usage
  900. if (hasInvalidNestingSelector(selector)) {
  901. return false;
  902. }
  903. // Check for invalid pseudo-class usage with quoted strings
  904. // Pseudo-classes like :lang(), :dir(), :nth-*() should not accept quoted strings
  905. // Using iterative parsing instead of regex to avoid exponential backtracking
  906. var noQuotesPseudos = ['lang', 'dir', 'nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type'];
  907. for (var idx = 0; idx < selector.length; idx++) {
  908. // Look for pseudo-class/element start
  909. if (selector[idx] === ':') {
  910. var pseudoStart = idx;
  911. idx++;
  912. // Skip second colon for pseudo-elements
  913. if (idx < selector.length && selector[idx] === ':') {
  914. idx++;
  915. }
  916. // Extract pseudo name
  917. var nameStart = idx;
  918. while (idx < selector.length && /[a-zA-Z0-9\-]/.test(selector[idx])) {
  919. idx++;
  920. }
  921. if (idx === nameStart) {
  922. continue; // No name found
  923. }
  924. var pseudoName = selector.substring(nameStart, idx).toLowerCase();
  925. // Check if this pseudo has arguments
  926. if (idx < selector.length && selector[idx] === '(') {
  927. idx++;
  928. var contentStart = idx;
  929. var depth = 1;
  930. // Find matching closing paren (handle nesting)
  931. while (idx < selector.length && depth > 0) {
  932. if (selector[idx] === '\\') {
  933. idx += 2; // Skip escaped character
  934. } else if (selector[idx] === '(') {
  935. depth++;
  936. idx++;
  937. } else if (selector[idx] === ')') {
  938. depth--;
  939. idx++;
  940. } else {
  941. idx++;
  942. }
  943. }
  944. if (depth === 0) {
  945. var pseudoContent = selector.substring(contentStart, idx - 1);
  946. // Check if this pseudo should not have quoted strings
  947. for (var j = 0; j < noQuotesPseudos.length; j++) {
  948. if (pseudoName === noQuotesPseudos[j] && /['"]/.test(pseudoContent)) {
  949. return false;
  950. }
  951. }
  952. }
  953. }
  954. }
  955. }
  956. // Use the iterative validator to avoid regex backtracking issues
  957. return looseSelectorValidator(selector);
  958. }
  959. /**
  960. * Regular expression to match CSS pseudo-classes with arguments.
  961. *
  962. * Matches patterns like `:pseudo-class(argument)`, capturing the pseudo-class name and its argument.
  963. *
  964. * Capture groups:
  965. * 1. The pseudo-class name (letters and hyphens).
  966. * 2. The argument inside the parentheses (can contain nested parentheses, quoted strings, and other characters.).
  967. *
  968. * Global flag (`g`) is used to find all matches in the input string.
  969. *
  970. * Example matches:
  971. * - :nth-child(2n+1)
  972. * - :has(.sel:nth-child(3n))
  973. * - :not(".foo, .bar")
  974. *
  975. * REPLACED WITH FUNCTION to avoid exponential backtracking.
  976. */
  977. /**
  978. * Extract pseudo-classes with arguments from a selector using iterative parsing.
  979. * Replaces the previous globalPseudoClassRegExp to avoid exponential backtracking.
  980. *
  981. * Handles:
  982. * - Regular content without parentheses or quotes
  983. * - Single-quoted strings
  984. * - Double-quoted strings
  985. * - Nested parentheses (arbitrary depth)
  986. *
  987. * @param {string} selector - The CSS selector to parse
  988. * @returns {Array} Array of matches, each with: [fullMatch, pseudoName, pseudoArgs, startIndex]
  989. */
  990. function extractPseudoClasses(selector) {
  991. var matches = [];
  992. for (var i = 0; i < selector.length; i++) {
  993. // Look for pseudo-class start (single or double colon)
  994. if (selector[i] === ':') {
  995. var pseudoStart = i;
  996. i++;
  997. // Skip second colon for pseudo-elements (::)
  998. if (i < selector.length && selector[i] === ':') {
  999. i++;
  1000. }
  1001. // Extract pseudo name
  1002. var nameStart = i;
  1003. while (i < selector.length && /[a-zA-Z\-]/.test(selector[i])) {
  1004. i++;
  1005. }
  1006. if (i === nameStart) {
  1007. continue; // No name found
  1008. }
  1009. var pseudoName = selector.substring(nameStart, i);
  1010. // Check if this pseudo has arguments
  1011. if (i < selector.length && selector[i] === '(') {
  1012. i++;
  1013. var argsStart = i;
  1014. var depth = 1;
  1015. var inSingleQuote = false;
  1016. var inDoubleQuote = false;
  1017. // Find matching closing paren (handle nesting and strings)
  1018. while (i < selector.length && depth > 0) {
  1019. var ch = selector[i];
  1020. if (ch === '\\') {
  1021. i += 2; // Skip escaped character
  1022. } else if (ch === "'" && !inDoubleQuote) {
  1023. inSingleQuote = !inSingleQuote;
  1024. i++;
  1025. } else if (ch === '"' && !inSingleQuote) {
  1026. inDoubleQuote = !inDoubleQuote;
  1027. i++;
  1028. } else if (ch === '(' && !inSingleQuote && !inDoubleQuote) {
  1029. depth++;
  1030. i++;
  1031. } else if (ch === ')' && !inSingleQuote && !inDoubleQuote) {
  1032. depth--;
  1033. i++;
  1034. } else {
  1035. i++;
  1036. }
  1037. }
  1038. if (depth === 0) {
  1039. var pseudoArgs = selector.substring(argsStart, i - 1);
  1040. var fullMatch = selector.substring(pseudoStart, i);
  1041. // Store match in same format as regex: [fullMatch, pseudoName, pseudoArgs, startIndex]
  1042. matches.push([fullMatch, pseudoName, pseudoArgs, pseudoStart]);
  1043. }
  1044. // Move back one since loop will increment
  1045. i--;
  1046. }
  1047. }
  1048. }
  1049. return matches;
  1050. }
  1051. /**
  1052. * Parses a CSS selector string and splits it into parts, handling nested parentheses.
  1053. *
  1054. * This function is useful for splitting selectors that may contain nested function-like
  1055. * syntax (e.g., :not(.foo, .bar)), ensuring that commas inside parentheses do not split
  1056. * the selector.
  1057. *
  1058. * @param {string} selector - The CSS selector string to parse.
  1059. * @returns {string[]} An array of selector parts, split by top-level commas, with whitespace trimmed.
  1060. */
  1061. function parseAndSplitNestedSelectors(selector) {
  1062. var depth = 0; // Track parenthesis nesting depth
  1063. var buffer = ""; // Accumulate characters for current selector part
  1064. var parts = []; // Array of split selector parts
  1065. var inSingleQuote = false; // Track if we're inside single quotes
  1066. var inDoubleQuote = false; // Track if we're inside double quotes
  1067. var i, char;
  1068. for (i = 0; i < selector.length; i++) {
  1069. char = selector.charAt(i);
  1070. // Handle escape sequences - skip them entirely
  1071. if (char === '\\' && i + 1 < selector.length) {
  1072. buffer += char;
  1073. i++;
  1074. buffer += selector.charAt(i);
  1075. continue;
  1076. }
  1077. // Handle single quote strings
  1078. if (char === "'" && !inDoubleQuote) {
  1079. inSingleQuote = !inSingleQuote;
  1080. buffer += char;
  1081. }
  1082. // Handle double quote strings
  1083. else if (char === '"' && !inSingleQuote) {
  1084. inDoubleQuote = !inDoubleQuote;
  1085. buffer += char;
  1086. }
  1087. // Process characters outside of quoted strings
  1088. else if (!inSingleQuote && !inDoubleQuote) {
  1089. if (char === '(') {
  1090. // Entering a nested level (e.g., :is(...))
  1091. depth++;
  1092. buffer += char;
  1093. } else if (char === ')') {
  1094. // Exiting a nested level
  1095. depth--;
  1096. buffer += char;
  1097. } else if (char === ',' && depth === 0) {
  1098. // Found a top-level comma separator - split here
  1099. // Note: escaped commas (\,) are already handled above
  1100. if (buffer.trim()) {
  1101. parts.push(buffer.trim());
  1102. }
  1103. buffer = "";
  1104. } else {
  1105. // Regular character - add to buffer
  1106. buffer += char;
  1107. }
  1108. }
  1109. // Characters inside quoted strings - add to buffer
  1110. else {
  1111. buffer += char;
  1112. }
  1113. }
  1114. // Add any remaining content in buffer as the last part
  1115. var trimmed = buffer.trim();
  1116. if (trimmed) {
  1117. // Preserve trailing space if selector ends with hex escape
  1118. var endsWithHexEscape = endsWithHexEscapeRegExp.test(buffer);
  1119. parts.push(endsWithHexEscape ? buffer.replace(leadingWhitespaceRegExp, '') : trimmed);
  1120. }
  1121. return parts;
  1122. }
  1123. /**
  1124. * Validates a CSS selector string, including handling of nested selectors within certain pseudo-classes.
  1125. *
  1126. * This function checks if the provided selector is valid according to the rules defined by
  1127. * `basicSelectorValidator`. For pseudo-classes that accept selector lists (such as :not, :is, :has, :where),
  1128. * it recursively validates each nested selector using the same validation logic.
  1129. *
  1130. * @param {string} selector - The CSS selector string to validate.
  1131. * @returns {boolean} Returns `true` if the selector is valid, otherwise `false`.
  1132. */
  1133. // Cache to store validated selectors (previously a ES6 Map, now an ES5-compliant object)
  1134. var validatedSelectorsCache = {};
  1135. // Only pseudo-classes that accept selector lists should recurse
  1136. var selectorListPseudoClasses = {
  1137. 'not': true,
  1138. 'is': true,
  1139. 'has': true,
  1140. 'where': true
  1141. };
  1142. function validateSelector(selector) {
  1143. if (validatedSelectorsCache.hasOwnProperty(selector)) {
  1144. return validatedSelectorsCache[selector];
  1145. }
  1146. // Use function-based parsing to extract pseudo-classes (avoids backtracking)
  1147. var pseudoClassMatches = extractPseudoClasses(selector);
  1148. for (var j = 0; j < pseudoClassMatches.length; j++) {
  1149. var pseudoClass = pseudoClassMatches[j][1];
  1150. if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) {
  1151. var nestedSelectors = parseAndSplitNestedSelectors(pseudoClassMatches[j][2]);
  1152. // Check if ANY selector in the list contains & (nesting selector)
  1153. // If so, skip validation for the entire selector list since & will be replaced at runtime
  1154. var hasAmpersand = false;
  1155. for (var k = 0; k < nestedSelectors.length; k++) {
  1156. if (ampersandRegExp.test(nestedSelectors[k])) {
  1157. hasAmpersand = true;
  1158. break;
  1159. }
  1160. }
  1161. // If any selector has &, skip validation for this entire pseudo-class
  1162. if (hasAmpersand) {
  1163. continue;
  1164. }
  1165. // Otherwise, validate each selector normally
  1166. for (var i = 0; i < nestedSelectors.length; i++) {
  1167. var nestedSelector = nestedSelectors[i];
  1168. if (!validatedSelectorsCache.hasOwnProperty(nestedSelector)) {
  1169. var nestedSelectorValidation = validateSelector(nestedSelector);
  1170. validatedSelectorsCache[nestedSelector] = nestedSelectorValidation;
  1171. if (!nestedSelectorValidation) {
  1172. validatedSelectorsCache[selector] = false;
  1173. return false;
  1174. }
  1175. } else if (!validatedSelectorsCache[nestedSelector]) {
  1176. validatedSelectorsCache[selector] = false;
  1177. return false;
  1178. }
  1179. }
  1180. }
  1181. }
  1182. var basicSelectorValidation = basicSelectorValidator(selector);
  1183. validatedSelectorsCache[selector] = basicSelectorValidation;
  1184. return basicSelectorValidation;
  1185. }
  1186. /**
  1187. * Validates namespace selectors by checking if the namespace prefix is defined.
  1188. *
  1189. * @param {string} selector - The CSS selector to validate
  1190. * @returns {boolean} Returns true if the namespace is valid, false otherwise
  1191. */
  1192. function validateNamespaceSelector(selector) {
  1193. // Check if selector contains a namespace prefix
  1194. // We need to ignore pipes inside attribute selectors
  1195. var pipeIndex = -1;
  1196. var inAttr = false;
  1197. var inSingleQuote = false;
  1198. var inDoubleQuote = false;
  1199. for (var i = 0; i < selector.length; i++) {
  1200. var char = selector[i];
  1201. // Handle escape sequences - skip hex escapes or simple escapes
  1202. if (char === '\\') {
  1203. var escapeLen = getEscapeSequenceLength(selector, i);
  1204. if (escapeLen > 0) {
  1205. i += escapeLen - 1; // -1 because loop will increment
  1206. continue;
  1207. }
  1208. }
  1209. if (inSingleQuote) {
  1210. if (char === "'") {
  1211. inSingleQuote = false;
  1212. }
  1213. } else if (inDoubleQuote) {
  1214. if (char === '"') {
  1215. inDoubleQuote = false;
  1216. }
  1217. } else if (inAttr) {
  1218. if (char === "]") {
  1219. inAttr = false;
  1220. } else if (char === "'") {
  1221. inSingleQuote = true;
  1222. } else if (char === '"') {
  1223. inDoubleQuote = true;
  1224. }
  1225. } else {
  1226. if (char === "[") {
  1227. inAttr = true;
  1228. } else if (char === "|" && !inAttr) {
  1229. // This is a namespace separator, not an attribute operator
  1230. pipeIndex = i;
  1231. break;
  1232. }
  1233. }
  1234. }
  1235. if (pipeIndex === -1) {
  1236. return true; // No namespace, always valid
  1237. }
  1238. var namespacePrefix = selector.substring(0, pipeIndex);
  1239. // Universal namespace (*|) and default namespace (|) are always valid
  1240. if (namespacePrefix === '*' || namespacePrefix === '') {
  1241. return true;
  1242. }
  1243. // Check if the custom namespace prefix is defined
  1244. return definedNamespacePrefixes.hasOwnProperty(namespacePrefix);
  1245. }
  1246. /**
  1247. * Normalizes escape sequences in a selector to match browser behavior.
  1248. * Decodes escape sequences and re-encodes them in canonical form.
  1249. *
  1250. * @param {string} selector - The selector to normalize
  1251. * @returns {string} Normalized selector
  1252. */
  1253. function normalizeSelectorEscapes(selector) {
  1254. var result = '';
  1255. var i = 0;
  1256. var nextChar = '';
  1257. // Track context for identifier boundaries
  1258. var inIdentifier = false;
  1259. var inAttribute = false;
  1260. var attributeDepth = 0;
  1261. var needsEscapeForIdent = false;
  1262. var lastWasHexEscape = false;
  1263. while (i < selector.length) {
  1264. var char = selector[i];
  1265. // Track attribute selector context
  1266. if (char === '[' && !inAttribute) {
  1267. inAttribute = true;
  1268. attributeDepth = 1;
  1269. result += char;
  1270. i++;
  1271. needsEscapeForIdent = false;
  1272. inIdentifier = false;
  1273. lastWasHexEscape = false;
  1274. continue;
  1275. }
  1276. if (inAttribute) {
  1277. if (char === '[') attributeDepth++;
  1278. if (char === ']') {
  1279. attributeDepth--;
  1280. if (attributeDepth === 0) inAttribute = false;
  1281. }
  1282. // Don't normalize escapes inside attribute selectors
  1283. if (char === '\\' && i + 1 < selector.length) {
  1284. var escapeLen = getEscapeSequenceLength(selector, i);
  1285. result += selector.substr(i, escapeLen);
  1286. i += escapeLen;
  1287. } else {
  1288. result += char;
  1289. i++;
  1290. }
  1291. lastWasHexEscape = false;
  1292. continue;
  1293. }
  1294. // Handle escape sequences
  1295. if (char === '\\') {
  1296. var escapeLen = getEscapeSequenceLength(selector, i);
  1297. if (escapeLen > 0) {
  1298. var escapeSeq = selector.substr(i, escapeLen);
  1299. var decoded = decodeEscapeSequence(escapeSeq);
  1300. var wasHexEscape = startsWithHexEscapeRegExp.test(escapeSeq);
  1301. var hadTerminatingSpace = wasHexEscape && escapeSeq[escapeLen - 1] === ' ';
  1302. nextChar = selector[i + escapeLen] || '';
  1303. // Check if this character needs escaping
  1304. var needsEscape = false;
  1305. var useHexEscape = false;
  1306. if (needsEscapeForIdent) {
  1307. // At start of identifier (after . # or -)
  1308. // Digits must be escaped, letters/underscore/_/- don't need escaping
  1309. if (isDigit(decoded)) {
  1310. needsEscape = true;
  1311. useHexEscape = true;
  1312. } else if (decoded === '-') {
  1313. // Dash at identifier start: keep escaped if it's the only character,
  1314. // otherwise it can be decoded
  1315. var remainingSelector = selector.substring(i + escapeLen);
  1316. var hasMoreIdentChars = remainingSelector && identCharRegExp.test(remainingSelector[0]);
  1317. needsEscape = !hasMoreIdentChars;
  1318. } else if (!identStartCharRegExp.test(decoded)) {
  1319. needsEscape = true;
  1320. }
  1321. } else {
  1322. if (specialCharsNeedEscapeRegExp.test(decoded)) {
  1323. needsEscape = true;
  1324. }
  1325. }
  1326. if (needsEscape) {
  1327. if (useHexEscape) {
  1328. // Use normalized hex escape
  1329. var codePoint = decoded.charCodeAt(0);
  1330. var hex = codePoint.toString(16);
  1331. result += '\\' + hex;
  1332. // Add space if next char could continue the hex sequence,
  1333. // or if at end of selector (to disambiguate the escape)
  1334. if (isHexDigit(nextChar) || !nextChar || afterHexEscapeSeparatorRegExp.test(nextChar)) {
  1335. result += ' ';
  1336. lastWasHexEscape = false;
  1337. } else {
  1338. lastWasHexEscape = true;
  1339. }
  1340. } else {
  1341. // Use simple character escape
  1342. result += '\\' + decoded;
  1343. lastWasHexEscape = false;
  1344. }
  1345. } else {
  1346. // No escape needed, use the character directly
  1347. // But if previous was hex escape (without terminating space) and this is alphanumeric, add space
  1348. if (lastWasHexEscape && !hadTerminatingSpace && isAlphanumeric(decoded)) {
  1349. result += ' ';
  1350. }
  1351. result += decoded;
  1352. // Preserve terminating space at end of selector (when followed by non-ident char)
  1353. if (hadTerminatingSpace && (!nextChar || afterHexEscapeSeparatorRegExp.test(nextChar))) {
  1354. result += ' ';
  1355. }
  1356. lastWasHexEscape = false;
  1357. }
  1358. i += escapeLen;
  1359. // After processing escape, check if we're still needing ident validation
  1360. // Only stay in needsEscapeForIdent state if decoded was '-'
  1361. needsEscapeForIdent = needsEscapeForIdent && decoded === '-';
  1362. inIdentifier = true;
  1363. continue;
  1364. }
  1365. }
  1366. // Handle regular characters
  1367. if (char === '.' || char === '#') {
  1368. result += char;
  1369. needsEscapeForIdent = true;
  1370. inIdentifier = false;
  1371. lastWasHexEscape = false;
  1372. i++;
  1373. } else if (char === '-' && needsEscapeForIdent) {
  1374. // Dash after . or # - next char must be valid ident start or digit (which needs escaping)
  1375. result += char;
  1376. needsEscapeForIdent = true;
  1377. lastWasHexEscape = false;
  1378. i++;
  1379. } else if (isDigit(char) && needsEscapeForIdent) {
  1380. // Digit at identifier start must be hex escaped
  1381. var codePoint = char.charCodeAt(0);
  1382. var hex = codePoint.toString(16);
  1383. result += '\\' + hex;
  1384. nextChar = selector[i + 1] || '';
  1385. // Add space if next char could continue the hex sequence,
  1386. // or if at end of selector (to disambiguate the escape)
  1387. if (isHexDigit(nextChar) || !nextChar || afterHexEscapeSeparatorRegExp.test(nextChar)) {
  1388. result += ' ';
  1389. lastWasHexEscape = false;
  1390. } else {
  1391. lastWasHexEscape = true;
  1392. }
  1393. needsEscapeForIdent = false;
  1394. inIdentifier = true;
  1395. i++;
  1396. } else if (char === ':' || combinatorOrSeparatorRegExp.test(char)) {
  1397. // Combinators, separators, and pseudo-class markers reset identifier state
  1398. // Preserve trailing space from hex escape
  1399. if (!(char === ' ' && lastWasHexEscape && result[result.length - 1] === ' ')) {
  1400. result += char;
  1401. }
  1402. needsEscapeForIdent = false;
  1403. inIdentifier = false;
  1404. lastWasHexEscape = false;
  1405. i++;
  1406. } else if (isLetter(char) && lastWasHexEscape) {
  1407. // Letter after hex escape needs a space separator
  1408. result += ' ' + char;
  1409. needsEscapeForIdent = false;
  1410. inIdentifier = true;
  1411. lastWasHexEscape = false;
  1412. i++;
  1413. } else if (char === ' ' && lastWasHexEscape) {
  1414. // Trailing space - keep it if at end or before non-ident char
  1415. nextChar = selector[i + 1] || '';
  1416. if (!nextChar || trailingSpaceSeparatorRegExp.test(nextChar)) {
  1417. result += char;
  1418. }
  1419. needsEscapeForIdent = false;
  1420. inIdentifier = false;
  1421. lastWasHexEscape = false;
  1422. i++;
  1423. } else {
  1424. result += char;
  1425. needsEscapeForIdent = false;
  1426. inIdentifier = true;
  1427. lastWasHexEscape = false;
  1428. i++;
  1429. }
  1430. }
  1431. return result;
  1432. }
  1433. /**
  1434. * Helper function to decode all escape sequences in a string.
  1435. *
  1436. * @param {string} str - The string to decode
  1437. * @returns {string} The decoded string
  1438. */
  1439. function decodeEscapeSequencesInString(str) {
  1440. var result = '';
  1441. for (var i = 0; i < str.length; i++) {
  1442. if (str[i] === '\\' && i + 1 < str.length) {
  1443. // Get the escape sequence length
  1444. var escapeLen = getEscapeSequenceLength(str, i);
  1445. if (escapeLen > 0) {
  1446. var escapeSeq = str.substr(i, escapeLen);
  1447. var decoded = decodeEscapeSequence(escapeSeq);
  1448. result += decoded;
  1449. i += escapeLen - 1; // -1 because loop will increment
  1450. continue;
  1451. }
  1452. }
  1453. result += str[i];
  1454. }
  1455. return result;
  1456. }
  1457. /**
  1458. * Decodes a CSS escape sequence to its character value.
  1459. *
  1460. * @param {string} escapeSeq - The escape sequence (including backslash)
  1461. * @returns {string} The decoded character
  1462. */
  1463. function decodeEscapeSequence(escapeSeq) {
  1464. if (escapeSeq.length < 2 || escapeSeq[0] !== '\\') {
  1465. return escapeSeq;
  1466. }
  1467. var content = escapeSeq.substring(1);
  1468. // Check if it's a hex escape
  1469. var hexMatch = content.match(hexEscapeSequenceRegExp);
  1470. if (hexMatch) {
  1471. var codePoint = parseInt(hexMatch[1], 16);
  1472. // Handle surrogate pairs for code points > 0xFFFF
  1473. if (codePoint > 0xFFFF) {
  1474. // Convert to surrogate pair
  1475. codePoint -= 0x10000;
  1476. var high = 0xD800 + (codePoint >> 10);
  1477. var low = 0xDC00 + (codePoint & 0x3FF);
  1478. return String.fromCharCode(high, low);
  1479. }
  1480. return String.fromCharCode(codePoint);
  1481. }
  1482. // Simple escape - return the character after backslash
  1483. return content[0] || '';
  1484. }
  1485. /**
  1486. * Normalizes attribute selectors by ensuring values are properly quoted with double quotes.
  1487. * Examples:
  1488. * [attr=value] -> [attr="value"]
  1489. * [attr="value"] -> [attr="value"] (unchanged)
  1490. * [attr='value'] -> [attr="value"] (converted to double quotes)
  1491. *
  1492. * @param {string} selector - The selector to normalize
  1493. * @returns {string|null} Normalized selector, or null if invalid
  1494. */
  1495. function normalizeAttributeSelectors(selector) {
  1496. var result = '';
  1497. var i = 0;
  1498. while (i < selector.length) {
  1499. // Look for attribute selector start
  1500. if (selector[i] === '[') {
  1501. result += '[';
  1502. i++;
  1503. var attrContent = '';
  1504. var depth = 1;
  1505. // Find the closing bracket, handling nested brackets and escapes
  1506. while (i < selector.length && depth > 0) {
  1507. if (selector[i] === '\\' && i + 1 < selector.length) {
  1508. attrContent += selector.substring(i, i + 2);
  1509. i += 2;
  1510. continue;
  1511. }
  1512. if (selector[i] === '[') depth++;
  1513. if (selector[i] === ']') {
  1514. depth--;
  1515. if (depth === 0) break;
  1516. }
  1517. attrContent += selector[i];
  1518. i++;
  1519. }
  1520. // Normalize the attribute content
  1521. var normalized = normalizeAttributeContent(attrContent);
  1522. if (normalized === null) {
  1523. // Invalid attribute selector (e.g., unclosed quote)
  1524. return null;
  1525. }
  1526. result += normalized;
  1527. if (i < selector.length && selector[i] === ']') {
  1528. result += ']';
  1529. i++;
  1530. }
  1531. } else {
  1532. result += selector[i];
  1533. i++;
  1534. }
  1535. }
  1536. return result;
  1537. }
  1538. /**
  1539. * Processes a quoted attribute value by checking for proper closure and decoding escape sequences.
  1540. * @param {string} trimmedValue - The quoted value (with quotes)
  1541. * @param {string} quoteChar - The quote character ('"' or "'")
  1542. * @param {string} attrName - The attribute name
  1543. * @param {string} operator - The attribute operator
  1544. * @param {string} flag - Optional case-sensitivity flag
  1545. * @returns {string|null} Normalized attribute content, or null if invalid
  1546. */
  1547. function processQuotedAttributeValue(trimmedValue, quoteChar, attrName, operator, flag) {
  1548. // Check if the closing quote is properly closed (not escaped)
  1549. if (trimmedValue.length < 2) {
  1550. return null; // Too short
  1551. }
  1552. // Find the actual closing quote (not escaped)
  1553. var i = 1;
  1554. var foundClose = false;
  1555. while (i < trimmedValue.length) {
  1556. if (trimmedValue[i] === '\\' && i + 1 < trimmedValue.length) {
  1557. // Skip escape sequence
  1558. var escapeLen = getEscapeSequenceLength(trimmedValue, i);
  1559. i += escapeLen;
  1560. continue;
  1561. }
  1562. if (trimmedValue[i] === quoteChar) {
  1563. // Found closing quote
  1564. foundClose = (i === trimmedValue.length - 1);
  1565. break;
  1566. }
  1567. i++;
  1568. }
  1569. if (!foundClose) {
  1570. return null; // Unclosed quote - invalid
  1571. }
  1572. // Extract inner value and decode escape sequences
  1573. var innerValue = trimmedValue.slice(1, -1);
  1574. var decodedValue = decodeEscapeSequencesInString(innerValue);
  1575. // If decoded value contains quotes, we need to escape them
  1576. var escapedValue = decodedValue.replace(doubleQuoteRegExp, '\\"');
  1577. return attrName + operator + '"' + escapedValue + '"' + (flag ? ' ' + flag : '');
  1578. }
  1579. /**
  1580. * Normalizes the content inside an attribute selector.
  1581. * @param {string} content - The content between [ and ]
  1582. * @returns {string} Normalized content, or null if invalid
  1583. */
  1584. function normalizeAttributeContent(content) {
  1585. // Match: attribute-name [operator] [value] [flag]
  1586. var match = content.match(attributeSelectorContentRegExp);
  1587. if (!match) {
  1588. // No operator (e.g., [disabled]) or malformed - return as is
  1589. return content;
  1590. }
  1591. var attrName = match[1];
  1592. var operator = match[2];
  1593. var valueAndFlag = match[3].trim(); // Trim here instead of in regex
  1594. // Check if there's a case-sensitivity flag (i or s) at the end
  1595. var flagMatch = valueAndFlag.match(attributeCaseFlagRegExp);
  1596. var value = flagMatch ? flagMatch[1] : valueAndFlag;
  1597. var flag = flagMatch ? flagMatch[2] : '';
  1598. // Check for unclosed quotes - this makes the selector invalid
  1599. var trimmedValue = value.trim();
  1600. var firstChar = trimmedValue[0];
  1601. if (firstChar === '"') {
  1602. return processQuotedAttributeValue(trimmedValue, '"', attrName, operator, flag);
  1603. }
  1604. if (firstChar === "'") {
  1605. return processQuotedAttributeValue(trimmedValue, "'", attrName, operator, flag);
  1606. }
  1607. // Check for unescaped special characters in unquoted values
  1608. // Escaped special characters are valid (e.g., \` is valid, but ` is not)
  1609. var hasUnescapedSpecialChar = false;
  1610. for (var i = 0; i < trimmedValue.length; i++) {
  1611. var char = trimmedValue[i];
  1612. if (char === '\\' && i + 1 < trimmedValue.length) {
  1613. // Skip the entire escape sequence
  1614. var escapeLen = getEscapeSequenceLength(trimmedValue, i);
  1615. if (escapeLen > 0) {
  1616. i += escapeLen - 1; // -1 because loop will increment
  1617. continue;
  1618. }
  1619. }
  1620. // Check if this is an unescaped special character
  1621. if (specialCharsNeedEscapeRegExp.test(char)) {
  1622. hasUnescapedSpecialChar = true;
  1623. break;
  1624. }
  1625. }
  1626. if (hasUnescapedSpecialChar) {
  1627. return null; // Unescaped special characters not allowed in unquoted attribute values
  1628. }
  1629. // Decode escape sequences in the value before quoting
  1630. // Inside quotes, special characters don't need escaping
  1631. var decodedValue = decodeEscapeSequencesInString(trimmedValue);
  1632. // If the decoded value contains double quotes, escape them for the output
  1633. // (since we're using double quotes as delimiters)
  1634. var escapedValue = decodedValue.replace(backslashRegExp, '\\\\').replace(doubleQuoteRegExp, '\\"');
  1635. // Unquoted value - add double quotes with decoded and re-escaped content
  1636. return attrName + operator + '"' + escapedValue + '"' + (flag ? ' ' + flag : '');
  1637. }
  1638. /**
  1639. * Processes a CSS selector text
  1640. *
  1641. * @param {string} selectorText - The CSS selector text to process
  1642. * @returns {string} The processed selector text with normalized whitespace and invalid selectors removed
  1643. */
  1644. function processSelectorText(selectorText) {
  1645. // Normalize whitespace first
  1646. var normalized = selectorText.replace(whitespaceNormalizationRegExp, function (match, _, newline) {
  1647. if (newline) return " ";
  1648. return match;
  1649. });
  1650. // Normalize escape sequences to match browser behavior
  1651. normalized = normalizeSelectorEscapes(normalized);
  1652. // Normalize attribute selectors (add quotes to unquoted values)
  1653. // Returns null if invalid (e.g., unclosed quotes)
  1654. normalized = normalizeAttributeSelectors(normalized);
  1655. if (normalized === null) {
  1656. return ''; // Invalid selector - return empty to trigger validation failure
  1657. }
  1658. // Recursively process pseudo-classes to handle nesting
  1659. return processNestedPseudoClasses(normalized);
  1660. }
  1661. /**
  1662. * Recursively processes pseudo-classes to filter invalid selectors
  1663. *
  1664. * @param {string} selectorText - The CSS selector text to process
  1665. * @param {number} depth - Current recursion depth (to prevent infinite loops)
  1666. * @returns {string} The processed selector text with invalid selectors removed
  1667. */
  1668. function processNestedPseudoClasses(selectorText, depth) {
  1669. // Prevent infinite recursion
  1670. if (typeof depth === 'undefined') {
  1671. depth = 0;
  1672. }
  1673. if (depth > 10) {
  1674. return selectorText;
  1675. }
  1676. var pseudoClassMatches = extractPseudoClasses(selectorText);
  1677. // If no pseudo-classes found, return as-is
  1678. if (pseudoClassMatches.length === 0) {
  1679. return selectorText;
  1680. }
  1681. // Build result by processing matches from right to left (to preserve positions)
  1682. var result = selectorText;
  1683. for (var j = pseudoClassMatches.length - 1; j >= 0; j--) {
  1684. var pseudoClass = pseudoClassMatches[j][1];
  1685. if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) {
  1686. var fullMatch = pseudoClassMatches[j][0];
  1687. var pseudoArgs = pseudoClassMatches[j][2];
  1688. var matchStart = pseudoClassMatches[j][3];
  1689. // Check if ANY selector contains & BEFORE processing
  1690. var nestedSelectorsRaw = parseAndSplitNestedSelectors(pseudoArgs);
  1691. var hasAmpersand = false;
  1692. for (var k = 0; k < nestedSelectorsRaw.length; k++) {
  1693. if (ampersandRegExp.test(nestedSelectorsRaw[k])) {
  1694. hasAmpersand = true;
  1695. break;
  1696. }
  1697. }
  1698. // If & is present, skip all processing (keep everything unchanged)
  1699. if (hasAmpersand) {
  1700. continue;
  1701. }
  1702. // Recursively process the arguments
  1703. var processedArgs = processNestedPseudoClasses(pseudoArgs, depth + 1);
  1704. var nestedSelectors = parseAndSplitNestedSelectors(processedArgs);
  1705. // Filter out invalid selectors
  1706. var validSelectors = [];
  1707. for (var i = 0; i < nestedSelectors.length; i++) {
  1708. var nestedSelector = nestedSelectors[i];
  1709. if (basicSelectorValidator(nestedSelector)) {
  1710. validSelectors.push(nestedSelector);
  1711. }
  1712. }
  1713. // Reconstruct the pseudo-class with only valid selectors
  1714. var newArgs = validSelectors.join(', ');
  1715. var newPseudoClass = ':' + pseudoClass + '(' + newArgs + ')';
  1716. // Replace in the result string using position (processing right to left preserves positions)
  1717. result = result.substring(0, matchStart) + newPseudoClass + result.substring(matchStart + fullMatch.length);
  1718. }
  1719. }
  1720. return result;
  1721. return normalized;
  1722. }
  1723. /**
  1724. * Checks if a selector contains newlines inside quoted strings.
  1725. * Uses iterative parsing to avoid regex backtracking issues.
  1726. * @param {string} selectorText - The selector to check
  1727. * @returns {boolean} True if newlines found inside quotes
  1728. */
  1729. function hasNewlineInQuotedString(selectorText) {
  1730. for (var i = 0; i < selectorText.length; i++) {
  1731. var char = selectorText[i];
  1732. // Start of single-quoted string
  1733. if (char === "'") {
  1734. i++;
  1735. while (i < selectorText.length) {
  1736. if (selectorText[i] === '\\' && i + 1 < selectorText.length) {
  1737. // Skip escape sequence
  1738. i += 2;
  1739. continue;
  1740. }
  1741. if (selectorText[i] === "'") {
  1742. // End of string
  1743. break;
  1744. }
  1745. if (selectorText[i] === '\r' || selectorText[i] === '\n') {
  1746. return true;
  1747. }
  1748. i++;
  1749. }
  1750. }
  1751. // Start of double-quoted string
  1752. else if (char === '"') {
  1753. i++;
  1754. while (i < selectorText.length) {
  1755. if (selectorText[i] === '\\' && i + 1 < selectorText.length) {
  1756. // Skip escape sequence
  1757. i += 2;
  1758. continue;
  1759. }
  1760. if (selectorText[i] === '"') {
  1761. // End of string
  1762. break;
  1763. }
  1764. if (selectorText[i] === '\r' || selectorText[i] === '\n') {
  1765. return true;
  1766. }
  1767. i++;
  1768. }
  1769. }
  1770. }
  1771. return false;
  1772. }
  1773. /**
  1774. * Checks if a given CSS selector text is valid by splitting it by commas
  1775. * and validating each individual selector using the `validateSelector` function.
  1776. *
  1777. * @param {string} selectorText - The CSS selector text to validate. Can contain multiple selectors separated by commas.
  1778. * @returns {boolean} Returns true if all selectors are valid, otherwise false.
  1779. */
  1780. function isValidSelectorText(selectorText) {
  1781. // TODO: The same validations here needs to be reused in CSSStyleRule.selectorText setter
  1782. // TODO: Move these validation logic to a shared function to be reused in CSSStyleRule.selectorText setter
  1783. // Check for empty or whitespace-only selector
  1784. if (!selectorText || selectorText.trim() === '') {
  1785. return false;
  1786. }
  1787. // Check for empty selector lists in pseudo-classes (e.g., :is(), :not(), :where(), :has())
  1788. // These are invalid after filtering out invalid selectors
  1789. if (emptyPseudoClassRegExp.test(selectorText)) {
  1790. return false;
  1791. }
  1792. // Check for newlines inside single or double quotes
  1793. // Uses helper function to avoid regex security issues
  1794. if (hasNewlineInQuotedString(selectorText)) {
  1795. return false;
  1796. }
  1797. // Split selectorText by commas and validate each part
  1798. var selectors = parseAndSplitNestedSelectors(selectorText);
  1799. for (var i = 0; i < selectors.length; i++) {
  1800. var selector = selectors[i].trim();
  1801. if (!validateSelector(selector) || !validateNamespaceSelector(selector)) {
  1802. return false;
  1803. }
  1804. }
  1805. return true;
  1806. }
  1807. function pushToAncestorRules(rule) {
  1808. ancestorRules.push(rule);
  1809. }
  1810. function parseError(message, isNested) {
  1811. var lines = token.substring(0, i).split('\n');
  1812. var lineCount = lines.length;
  1813. var charCount = lines.pop().length + 1;
  1814. var error = new Error(message + ' (line ' + lineCount + ', char ' + charCount + ')');
  1815. error.line = lineCount;
  1816. /* jshint sub : true */
  1817. error['char'] = charCount;
  1818. error.styleSheet = styleSheet;
  1819. error.isNested = !!isNested;
  1820. // Print the error but continue parsing the sheet
  1821. try {
  1822. throw error;
  1823. } catch (e) {
  1824. errorHandler && errorHandler(e);
  1825. }
  1826. };
  1827. /**
  1828. * Handles invalid selectors with unmatched quotes by skipping the entire rule block.
  1829. * @param {string} nextState - The parser state to transition to after skipping
  1830. */
  1831. function handleUnmatchedQuoteInSelector(nextState) {
  1832. // parseError('Invalid selector with unmatched quote: ' + buffer.trim());
  1833. // Skip this entire invalid rule including its block
  1834. var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
  1835. if (ruleClosingMatch) {
  1836. i += ruleClosingMatch.index + ruleClosingMatch[0].length - 1;
  1837. }
  1838. styleRule = null;
  1839. buffer = "";
  1840. hasUnmatchedQuoteInSelector = false; // Reset flag
  1841. state = nextState;
  1842. }
  1843. // Helper functions to check character types
  1844. function isSelectorStartChar(char) {
  1845. return '.:#&*['.indexOf(char) !== -1;
  1846. }
  1847. function isWhitespaceChar(char) {
  1848. return ' \t\n\r'.indexOf(char) !== -1;
  1849. }
  1850. // Helper functions for character type checking (faster than regex for single chars)
  1851. function isDigit(char) {
  1852. var code = char.charCodeAt(0);
  1853. return code >= 0x0030 && code <= 0x0039; // 0-9
  1854. }
  1855. function isHexDigit(char) {
  1856. if (!char) return false;
  1857. var code = char.charCodeAt(0);
  1858. return (code >= 0x0030 && code <= 0x0039) || // 0-9
  1859. (code >= 0x0041 && code <= 0x0046) || // A-F
  1860. (code >= 0x0061 && code <= 0x0066); // a-f
  1861. }
  1862. function isLetter(char) {
  1863. if (!char) return false;
  1864. var code = char.charCodeAt(0);
  1865. return (code >= 0x0041 && code <= 0x005A) || // A-Z
  1866. (code >= 0x0061 && code <= 0x007A); // a-z
  1867. }
  1868. function isAlphanumeric(char) {
  1869. var code = char.charCodeAt(0);
  1870. return (code >= 0x0030 && code <= 0x0039) || // 0-9
  1871. (code >= 0x0041 && code <= 0x005A) || // A-Z
  1872. (code >= 0x0061 && code <= 0x007A); // a-z
  1873. }
  1874. /**
  1875. * Get the length of an escape sequence starting at the given position.
  1876. * CSS escape sequences are:
  1877. * - Backslash followed by 1-6 hex digits, optionally followed by a whitespace (consumed)
  1878. * - Backslash followed by any non-hex character
  1879. * @param {string} str - The string to check
  1880. * @param {number} pos - Position of the backslash
  1881. * @returns {number} Number of characters in the escape sequence (including backslash)
  1882. */
  1883. function getEscapeSequenceLength(str, pos) {
  1884. if (str[pos] !== '\\' || pos + 1 >= str.length) {
  1885. return 0;
  1886. }
  1887. var nextChar = str[pos + 1];
  1888. // Check if it's a hex escape
  1889. if (isHexDigit(nextChar)) {
  1890. var hexLength = 1;
  1891. // Count up to 6 hex digits
  1892. while (hexLength < 6 && pos + 1 + hexLength < str.length && isHexDigit(str[pos + 1 + hexLength])) {
  1893. hexLength++;
  1894. }
  1895. // Check if followed by optional whitespace (which gets consumed)
  1896. if (pos + 1 + hexLength < str.length && isWhitespaceChar(str[pos + 1 + hexLength])) {
  1897. return 1 + hexLength + 1; // backslash + hex digits + whitespace
  1898. }
  1899. return 1 + hexLength; // backslash + hex digits
  1900. }
  1901. // Simple escape: backslash + any character
  1902. return 2;
  1903. }
  1904. /**
  1905. * Check if a string contains an unescaped occurrence of a specific character
  1906. * @param {string} str - The string to search
  1907. * @param {string} char - The character to look for
  1908. * @returns {boolean} True if the character appears unescaped
  1909. */
  1910. function containsUnescaped(str, char) {
  1911. for (var i = 0; i < str.length; i++) {
  1912. if (str[i] === '\\') {
  1913. var escapeLen = getEscapeSequenceLength(str, i);
  1914. if (escapeLen > 0) {
  1915. i += escapeLen - 1; // -1 because loop will increment
  1916. continue;
  1917. }
  1918. }
  1919. if (str[i] === char) {
  1920. return true;
  1921. }
  1922. }
  1923. return false;
  1924. }
  1925. var endingIndex = token.length - 1;
  1926. var initialEndingIndex = endingIndex;
  1927. for (var character; (character = token.charAt(i)); i++) {
  1928. if (i === endingIndex) {
  1929. switch (state) {
  1930. case "importRule":
  1931. case "namespaceRule":
  1932. case "layerBlock":
  1933. if (character !== ";") {
  1934. token += ";";
  1935. endingIndex += 1;
  1936. }
  1937. break;
  1938. case "value":
  1939. if (character !== "}") {
  1940. if (character === ";") {
  1941. token += "}"
  1942. } else {
  1943. token += ";";
  1944. }
  1945. endingIndex += 1;
  1946. break;
  1947. }
  1948. case "name":
  1949. case "before-name":
  1950. if (character === "}") {
  1951. token += " "
  1952. } else {
  1953. token += "}"
  1954. }
  1955. endingIndex += 1
  1956. break;
  1957. case "before-selector":
  1958. if (character !== "}" && currentScope !== styleSheet) {
  1959. token += "}"
  1960. endingIndex += 1
  1961. break;
  1962. }
  1963. }
  1964. }
  1965. // Handle escape sequences before processing special characters
  1966. // CSS escape sequences: \HHHHHH (1-6 hex digits) optionally followed by whitespace, or \ + any char
  1967. if (character === '\\' && i + 1 < token.length) {
  1968. var escapeLen = getEscapeSequenceLength(token, i);
  1969. if (escapeLen > 0) {
  1970. buffer += token.substr(i, escapeLen);
  1971. i += escapeLen - 1; // -1 because loop will increment
  1972. continue;
  1973. }
  1974. }
  1975. switch (character) {
  1976. case " ":
  1977. case "\t":
  1978. case "\r":
  1979. case "\n":
  1980. case "\f":
  1981. if (SIGNIFICANT_WHITESPACE[state]) {
  1982. buffer += character;
  1983. }
  1984. break;
  1985. // String
  1986. case '"':
  1987. index = i + 1;
  1988. do {
  1989. index = token.indexOf('"', index) + 1;
  1990. if (!index) {
  1991. parseError('Unmatched "');
  1992. // If we're parsing a selector, flag it as invalid
  1993. if (state === "selector" || state === "atRule") {
  1994. hasUnmatchedQuoteInSelector = true;
  1995. }
  1996. }
  1997. } while (token[index - 2] === '\\');
  1998. if (index === 0) {
  1999. break;
  2000. }
  2001. buffer += token.slice(i, index);
  2002. i = index - 1;
  2003. switch (state) {
  2004. case 'before-value':
  2005. state = 'value';
  2006. break;
  2007. case 'importRule-begin':
  2008. state = 'importRule';
  2009. if (i === endingIndex) {
  2010. token += ';'
  2011. }
  2012. break;
  2013. case 'namespaceRule-begin':
  2014. state = 'namespaceRule';
  2015. if (i === endingIndex) {
  2016. token += ';'
  2017. }
  2018. break;
  2019. }
  2020. break;
  2021. case "'":
  2022. index = i + 1;
  2023. do {
  2024. index = token.indexOf("'", index) + 1;
  2025. if (!index) {
  2026. parseError("Unmatched '");
  2027. // If we're parsing a selector, flag it as invalid
  2028. if (state === "selector" || state === "atRule") {
  2029. hasUnmatchedQuoteInSelector = true;
  2030. }
  2031. }
  2032. } while (token[index - 2] === '\\');
  2033. if (index === 0) {
  2034. break;
  2035. }
  2036. buffer += token.slice(i, index);
  2037. i = index - 1;
  2038. switch (state) {
  2039. case 'before-value':
  2040. state = 'value';
  2041. break;
  2042. case 'importRule-begin':
  2043. state = 'importRule';
  2044. break;
  2045. case 'namespaceRule-begin':
  2046. state = 'namespaceRule';
  2047. break;
  2048. }
  2049. break;
  2050. // Comment
  2051. case "/":
  2052. if (token.charAt(i + 1) === "*") {
  2053. i += 2;
  2054. index = token.indexOf("*/", i);
  2055. if (index === -1) {
  2056. i = token.length - 1;
  2057. buffer = "";
  2058. } else {
  2059. i = index + 1;
  2060. }
  2061. } else {
  2062. buffer += character;
  2063. }
  2064. if (state === "importRule-begin") {
  2065. buffer += " ";
  2066. state = "importRule";
  2067. }
  2068. if (state === "namespaceRule-begin") {
  2069. buffer += " ";
  2070. state = "namespaceRule";
  2071. }
  2072. break;
  2073. // At-rule
  2074. case "@":
  2075. if (nestedSelectorRule) {
  2076. if (styleRule && styleRule.constructor.name === "CSSNestedDeclarations") {
  2077. currentScope.cssRules.push(styleRule);
  2078. }
  2079. // Only reset styleRule to parent if styleRule is not the nestedSelectorRule itself
  2080. // This preserves nested selectors when followed immediately by @-rules
  2081. if (styleRule !== nestedSelectorRule && nestedSelectorRule.parentRule && nestedSelectorRule.parentRule.constructor.name === "CSSStyleRule") {
  2082. styleRule = nestedSelectorRule.parentRule;
  2083. }
  2084. // Don't reset nestedSelectorRule here - preserve it through @-rules
  2085. }
  2086. if (token.indexOf("@-moz-document", i) === i) {
  2087. validateAtRule("@-moz-document", function () {
  2088. state = "documentRule-begin";
  2089. documentRule = new CSSOM.CSSDocumentRule();
  2090. documentRule.__starts = i;
  2091. i += "-moz-document".length;
  2092. });
  2093. buffer = "";
  2094. break;
  2095. } else if (token.indexOf("@media", i) === i) {
  2096. validateAtRule("@media", function () {
  2097. state = "atBlock";
  2098. mediaRule = new CSSOM.CSSMediaRule();
  2099. mediaRule.__starts = i;
  2100. i += "media".length;
  2101. });
  2102. buffer = "";
  2103. break;
  2104. } else if (token.indexOf("@container", i) === i) {
  2105. validateAtRule("@container", function () {
  2106. state = "containerBlock";
  2107. containerRule = new CSSOM.CSSContainerRule();
  2108. containerRule.__starts = i;
  2109. i += "container".length;
  2110. });
  2111. buffer = "";
  2112. break;
  2113. } else if (token.indexOf("@counter-style", i) === i) {
  2114. buffer = "";
  2115. // @counter-style can be nested only inside CSSScopeRule or CSSConditionRule
  2116. // and only if there's no CSSStyleRule in the parent chain
  2117. var cannotBeNested = !canAtRuleBeNested();
  2118. validateAtRule("@counter-style", function () {
  2119. state = "counterStyleBlock"
  2120. counterStyleRule = new CSSOM.CSSCounterStyleRule();
  2121. counterStyleRule.__starts = i;
  2122. i += "counter-style".length;
  2123. }, cannotBeNested);
  2124. break;
  2125. } else if (token.indexOf("@property", i) === i) {
  2126. buffer = "";
  2127. // @property can be nested only inside CSSScopeRule or CSSConditionRule
  2128. // and only if there's no CSSStyleRule in the parent chain
  2129. var cannotBeNested = !canAtRuleBeNested();
  2130. validateAtRule("@property", function () {
  2131. state = "propertyBlock"
  2132. propertyRule = new CSSOM.CSSPropertyRule();
  2133. propertyRule.__starts = i;
  2134. i += "property".length;
  2135. }, cannotBeNested);
  2136. break;
  2137. } else if (token.indexOf("@scope", i) === i) {
  2138. validateAtRule("@scope", function () {
  2139. state = "scopeBlock";
  2140. scopeRule = new CSSOM.CSSScopeRule();
  2141. scopeRule.__starts = i;
  2142. i += "scope".length;
  2143. });
  2144. buffer = "";
  2145. break;
  2146. } else if (token.indexOf("@layer", i) === i) {
  2147. validateAtRule("@layer", function () {
  2148. state = "layerBlock"
  2149. layerBlockRule = new CSSOM.CSSLayerBlockRule();
  2150. layerBlockRule.__starts = i;
  2151. i += "layer".length;
  2152. });
  2153. buffer = "";
  2154. break;
  2155. } else if (token.indexOf("@page", i) === i) {
  2156. validateAtRule("@page", function () {
  2157. state = "pageBlock"
  2158. pageRule = new CSSOM.CSSPageRule();
  2159. pageRule.__starts = i;
  2160. i += "page".length;
  2161. });
  2162. buffer = "";
  2163. break;
  2164. } else if (token.indexOf("@supports", i) === i) {
  2165. validateAtRule("@supports", function () {
  2166. state = "conditionBlock";
  2167. supportsRule = new CSSOM.CSSSupportsRule();
  2168. supportsRule.__starts = i;
  2169. i += "supports".length;
  2170. });
  2171. buffer = "";
  2172. break;
  2173. } else if (token.indexOf("@host", i) === i) {
  2174. validateAtRule("@host", function () {
  2175. state = "hostRule-begin";
  2176. i += "host".length;
  2177. hostRule = new CSSOM.CSSHostRule();
  2178. hostRule.__starts = i;
  2179. });
  2180. buffer = "";
  2181. break;
  2182. } else if (token.indexOf("@starting-style", i) === i) {
  2183. validateAtRule("@starting-style", function () {
  2184. state = "startingStyleRule-begin";
  2185. i += "starting-style".length;
  2186. startingStyleRule = new CSSOM.CSSStartingStyleRule();
  2187. startingStyleRule.__starts = i;
  2188. });
  2189. buffer = "";
  2190. break;
  2191. } else if (token.indexOf("@import", i) === i) {
  2192. buffer = "";
  2193. validateAtRule("@import", function () {
  2194. state = "importRule-begin";
  2195. i += "import".length;
  2196. buffer += "@import";
  2197. }, true);
  2198. break;
  2199. } else if (token.indexOf("@namespace", i) === i) {
  2200. buffer = "";
  2201. validateAtRule("@namespace", function () {
  2202. state = "namespaceRule-begin";
  2203. i += "namespace".length;
  2204. buffer += "@namespace";
  2205. }, true);
  2206. break;
  2207. } else if (token.indexOf("@font-face", i) === i) {
  2208. buffer = "";
  2209. // @font-face can be nested only inside CSSScopeRule or CSSConditionRule
  2210. // and only if there's no CSSStyleRule in the parent chain
  2211. var cannotBeNested = !canAtRuleBeNested();
  2212. validateAtRule("@font-face", function () {
  2213. state = "fontFaceRule-begin";
  2214. i += "font-face".length;
  2215. fontFaceRule = new CSSOM.CSSFontFaceRule();
  2216. fontFaceRule.__starts = i;
  2217. }, cannotBeNested);
  2218. break;
  2219. } else {
  2220. // Reset lastIndex before using global regex (shared instance)
  2221. atKeyframesRegExp.lastIndex = i;
  2222. var matchKeyframes = atKeyframesRegExp.exec(token);
  2223. if (matchKeyframes && matchKeyframes.index === i) {
  2224. state = "keyframesRule-begin";
  2225. keyframesRule = new CSSOM.CSSKeyframesRule();
  2226. keyframesRule.__starts = i;
  2227. keyframesRule._vendorPrefix = matchKeyframes[1]; // Will come out as undefined if no prefix was found
  2228. i += matchKeyframes[0].length - 1;
  2229. buffer = "";
  2230. break;
  2231. } else if (state === "selector") {
  2232. state = "atRule";
  2233. }
  2234. }
  2235. buffer += character;
  2236. break;
  2237. case "{":
  2238. if (currentScope === topScope) {
  2239. nestedSelectorRule = null;
  2240. }
  2241. if (state === 'before-selector') {
  2242. parseError("Unexpected {");
  2243. i = ignoreBalancedBlock(i, token.slice(i));
  2244. break;
  2245. }
  2246. if (state === "selector" || state === "atRule") {
  2247. if (!nestedSelectorRule && containsUnescaped(buffer, ";")) {
  2248. var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
  2249. if (ruleClosingMatch) {
  2250. styleRule = null;
  2251. buffer = "";
  2252. state = "before-selector";
  2253. i += ruleClosingMatch.index + ruleClosingMatch[0].length;
  2254. break;
  2255. }
  2256. }
  2257. // Ensure styleRule exists before trying to set properties on it
  2258. if (!styleRule) {
  2259. styleRule = new CSSOM.CSSStyleRule();
  2260. styleRule.__starts = i;
  2261. }
  2262. // Check if tokenizer detected an unmatched quote BEFORE setting up the rule
  2263. if (hasUnmatchedQuoteInSelector) {
  2264. handleUnmatchedQuoteInSelector("before-selector");
  2265. break;
  2266. }
  2267. var originalParentRule = parentRule;
  2268. if (parentRule) {
  2269. styleRule.__parentRule = parentRule;
  2270. pushToAncestorRules(parentRule);
  2271. }
  2272. currentScope = parentRule = styleRule;
  2273. var processedSelectorText = processSelectorText(buffer.trim());
  2274. // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
  2275. if (originalParentRule && originalParentRule.constructor.name === "CSSStyleRule") {
  2276. styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function (sel) {
  2277. // Add & at the beginning if there's no & in the selector, or if it starts with a combinator
  2278. return (sel.indexOf('&') === -1 || startsWithCombinatorRegExp.test(sel)) ? '& ' + sel : sel;
  2279. }).join(', ');
  2280. } else {
  2281. // Normalize comma spacing: split by commas and rejoin with ", "
  2282. styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).join(', ');
  2283. }
  2284. styleRule.style.__starts = i;
  2285. styleRule.__parentStyleSheet = styleSheet;
  2286. buffer = "";
  2287. state = "before-name";
  2288. } else if (state === "atBlock") {
  2289. mediaRule.media.mediaText = buffer.trim();
  2290. if (parentRule) {
  2291. mediaRule.__parentRule = parentRule;
  2292. pushToAncestorRules(parentRule);
  2293. // If entering @media from within a CSSStyleRule, set nestedSelectorRule
  2294. // so that & selectors and declarations work correctly inside
  2295. if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
  2296. nestedSelectorRule = parentRule;
  2297. }
  2298. }
  2299. currentScope = parentRule = mediaRule;
  2300. pushToAncestorRules(mediaRule);
  2301. mediaRule.__parentStyleSheet = styleSheet;
  2302. // Don't reset styleRule to null if it's a nested CSSStyleRule that will contain this @-rule
  2303. if (!styleRule || styleRule.constructor.name !== "CSSStyleRule" || !styleRule.__parentRule) {
  2304. styleRule = null; // Reset styleRule when entering @-rule
  2305. }
  2306. buffer = "";
  2307. state = "before-selector";
  2308. } else if (state === "containerBlock") {
  2309. containerRule.__conditionText = buffer.trim();
  2310. if (parentRule) {
  2311. containerRule.__parentRule = parentRule;
  2312. pushToAncestorRules(parentRule);
  2313. if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
  2314. nestedSelectorRule = parentRule;
  2315. }
  2316. }
  2317. currentScope = parentRule = containerRule;
  2318. pushToAncestorRules(containerRule);
  2319. containerRule.__parentStyleSheet = styleSheet;
  2320. styleRule = null; // Reset styleRule when entering @-rule
  2321. buffer = "";
  2322. state = "before-selector";
  2323. } else if (state === "counterStyleBlock") {
  2324. var counterStyleName = buffer.trim().replace(newlineRemovalRegExp, "");
  2325. // Validate: name cannot be empty, contain whitespace, or contain dots
  2326. var isValidCounterStyleName = counterStyleName.length > 0 && !whitespaceAndDotRegExp.test(counterStyleName);
  2327. if (isValidCounterStyleName) {
  2328. counterStyleRule.name = counterStyleName;
  2329. if (parentRule) {
  2330. counterStyleRule.__parentRule = parentRule;
  2331. }
  2332. counterStyleRule.__parentStyleSheet = styleSheet;
  2333. styleRule = counterStyleRule;
  2334. }
  2335. buffer = "";
  2336. } else if (state === "propertyBlock") {
  2337. var propertyName = buffer.trim().replace(newlineRemovalRegExp, "");
  2338. // Validate: name must start with -- (custom property)
  2339. var isValidPropertyName = propertyName.indexOf("--") === 0;
  2340. if (isValidPropertyName) {
  2341. propertyRule.__name = propertyName;
  2342. if (parentRule) {
  2343. propertyRule.__parentRule = parentRule;
  2344. }
  2345. propertyRule.__parentStyleSheet = styleSheet;
  2346. styleRule = propertyRule;
  2347. }
  2348. buffer = "";
  2349. } else if (state === "conditionBlock") {
  2350. supportsRule.__conditionText = buffer.trim();
  2351. if (parentRule) {
  2352. supportsRule.__parentRule = parentRule;
  2353. pushToAncestorRules(parentRule);
  2354. if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
  2355. nestedSelectorRule = parentRule;
  2356. }
  2357. }
  2358. currentScope = parentRule = supportsRule;
  2359. pushToAncestorRules(supportsRule);
  2360. supportsRule.__parentStyleSheet = styleSheet;
  2361. styleRule = null; // Reset styleRule when entering @-rule
  2362. buffer = "";
  2363. state = "before-selector";
  2364. } else if (state === "scopeBlock") {
  2365. var parsedScopePrelude = parseScopePrelude(buffer.trim());
  2366. if (parsedScopePrelude.hasStart) {
  2367. scopeRule.__start = parsedScopePrelude.startSelector;
  2368. }
  2369. if (parsedScopePrelude.hasEnd) {
  2370. scopeRule.__end = parsedScopePrelude.endSelector;
  2371. }
  2372. if (parsedScopePrelude.hasOnlyEnd) {
  2373. scopeRule.__end = parsedScopePrelude.endSelector;
  2374. }
  2375. if (parentRule) {
  2376. scopeRule.__parentRule = parentRule;
  2377. pushToAncestorRules(parentRule);
  2378. if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
  2379. nestedSelectorRule = parentRule;
  2380. }
  2381. }
  2382. currentScope = parentRule = scopeRule;
  2383. pushToAncestorRules(scopeRule);
  2384. scopeRule.__parentStyleSheet = styleSheet;
  2385. styleRule = null; // Reset styleRule when entering @-rule
  2386. buffer = "";
  2387. state = "before-selector";
  2388. } else if (state === "layerBlock") {
  2389. layerBlockRule.name = buffer.trim();
  2390. var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(cssCustomIdentifierRegExp) !== null;
  2391. if (isValidName) {
  2392. if (parentRule) {
  2393. layerBlockRule.__parentRule = parentRule;
  2394. pushToAncestorRules(parentRule);
  2395. if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
  2396. nestedSelectorRule = parentRule;
  2397. }
  2398. }
  2399. currentScope = parentRule = layerBlockRule;
  2400. pushToAncestorRules(layerBlockRule);
  2401. layerBlockRule.__parentStyleSheet = styleSheet;
  2402. }
  2403. styleRule = null; // Reset styleRule when entering @-rule
  2404. buffer = "";
  2405. state = "before-selector";
  2406. } else if (state === "pageBlock") {
  2407. pageRule.selectorText = buffer.trim();
  2408. if (parentRule) {
  2409. pageRule.__parentRule = parentRule;
  2410. pushToAncestorRules(parentRule);
  2411. }
  2412. currentScope = parentRule = pageRule;
  2413. pageRule.__parentStyleSheet = styleSheet;
  2414. styleRule = pageRule;
  2415. buffer = "";
  2416. state = "before-name";
  2417. } else if (state === "hostRule-begin") {
  2418. if (parentRule) {
  2419. pushToAncestorRules(parentRule);
  2420. }
  2421. currentScope = parentRule = hostRule;
  2422. pushToAncestorRules(hostRule);
  2423. hostRule.__parentStyleSheet = styleSheet;
  2424. buffer = "";
  2425. state = "before-selector";
  2426. } else if (state === "startingStyleRule-begin") {
  2427. if (parentRule) {
  2428. startingStyleRule.__parentRule = parentRule;
  2429. pushToAncestorRules(parentRule);
  2430. if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
  2431. nestedSelectorRule = parentRule;
  2432. }
  2433. }
  2434. currentScope = parentRule = startingStyleRule;
  2435. pushToAncestorRules(startingStyleRule);
  2436. startingStyleRule.__parentStyleSheet = styleSheet;
  2437. styleRule = null; // Reset styleRule when entering @-rule
  2438. buffer = "";
  2439. state = "before-selector";
  2440. } else if (state === "fontFaceRule-begin") {
  2441. if (parentRule) {
  2442. fontFaceRule.__parentRule = parentRule;
  2443. }
  2444. fontFaceRule.__parentStyleSheet = styleSheet;
  2445. styleRule = fontFaceRule;
  2446. buffer = "";
  2447. state = "before-name";
  2448. } else if (state === "keyframesRule-begin") {
  2449. keyframesRule.name = buffer.trim();
  2450. if (parentRule) {
  2451. pushToAncestorRules(parentRule);
  2452. keyframesRule.__parentRule = parentRule;
  2453. }
  2454. keyframesRule.__parentStyleSheet = styleSheet;
  2455. currentScope = parentRule = keyframesRule;
  2456. buffer = "";
  2457. state = "keyframeRule-begin";
  2458. } else if (state === "keyframeRule-begin") {
  2459. styleRule = new CSSOM.CSSKeyframeRule();
  2460. styleRule.keyText = buffer.trim();
  2461. styleRule.__starts = i;
  2462. buffer = "";
  2463. state = "before-name";
  2464. } else if (state === "documentRule-begin") {
  2465. // FIXME: what if this '{' is in the url text of the match function?
  2466. documentRule.matcher.matcherText = buffer.trim();
  2467. if (parentRule) {
  2468. pushToAncestorRules(parentRule);
  2469. documentRule.__parentRule = parentRule;
  2470. }
  2471. currentScope = parentRule = documentRule;
  2472. pushToAncestorRules(documentRule);
  2473. documentRule.__parentStyleSheet = styleSheet;
  2474. buffer = "";
  2475. state = "before-selector";
  2476. } else if (state === "before-name" || state === "name") {
  2477. // @font-face and similar rules don't support nested selectors
  2478. // If we encounter a nested selector block inside them, skip it
  2479. if (styleRule.constructor.name === "CSSFontFaceRule" ||
  2480. styleRule.constructor.name === "CSSKeyframeRule" ||
  2481. (styleRule.constructor.name === "CSSPageRule" && parentRule === styleRule)) {
  2482. // Skip the nested block
  2483. var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
  2484. if (ruleClosingMatch) {
  2485. i += ruleClosingMatch.index + ruleClosingMatch[0].length - 1;
  2486. buffer = "";
  2487. state = "before-name";
  2488. break;
  2489. }
  2490. }
  2491. if (styleRule.constructor.name === "CSSNestedDeclarations") {
  2492. if (styleRule.style.length) {
  2493. parentRule.cssRules.push(styleRule);
  2494. styleRule.__parentRule = parentRule;
  2495. styleRule.__parentStyleSheet = styleSheet;
  2496. pushToAncestorRules(parentRule);
  2497. } else {
  2498. // If the styleRule is empty, we can assume that it's a nested selector
  2499. pushToAncestorRules(parentRule);
  2500. }
  2501. } else {
  2502. currentScope = parentRule = styleRule;
  2503. pushToAncestorRules(parentRule);
  2504. styleRule.__parentStyleSheet = styleSheet;
  2505. }
  2506. styleRule = new CSSOM.CSSStyleRule();
  2507. // Check if tokenizer detected an unmatched quote BEFORE setting up the rule
  2508. if (hasUnmatchedQuoteInSelector) {
  2509. handleUnmatchedQuoteInSelector("before-name");
  2510. break;
  2511. }
  2512. var processedSelectorText = processSelectorText(buffer.trim());
  2513. // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
  2514. if (parentRule.constructor.name === "CSSScopeRule" || (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null)) {
  2515. // Normalize comma spacing: split by commas and rejoin with ", "
  2516. styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).join(', ');
  2517. } else {
  2518. styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function (sel) {
  2519. // Add & at the beginning if there's no & in the selector, or if it starts with a combinator
  2520. return (sel.indexOf('&') === -1 || startsWithCombinatorRegExp.test(sel)) ? '& ' + sel : sel;
  2521. }).join(', ');
  2522. }
  2523. styleRule.style.__starts = i - buffer.length;
  2524. styleRule.__parentRule = parentRule;
  2525. // Only set nestedSelectorRule if we're directly inside a CSSStyleRule or CSSScopeRule,
  2526. // not inside other grouping rules like @media/@supports
  2527. if (parentRule.constructor.name === "CSSStyleRule" || parentRule.constructor.name === "CSSScopeRule") {
  2528. nestedSelectorRule = styleRule;
  2529. }
  2530. // Set __parentStyleSheet for the new nested styleRule
  2531. styleRule.__parentStyleSheet = styleSheet;
  2532. // Update currentScope and parentRule to the new nested styleRule
  2533. // so that subsequent content (like @-rules) will be children of this rule
  2534. currentScope = parentRule = styleRule;
  2535. buffer = "";
  2536. state = "before-name";
  2537. }
  2538. break;
  2539. case ":":
  2540. if (state === "name") {
  2541. // It can be a nested selector, let's check
  2542. var openBraceBeforeMatch = token.slice(i).match(declarationOrOpenBraceRegExp);
  2543. var hasOpenBraceBefore = openBraceBeforeMatch && openBraceBeforeMatch[0] === '{';
  2544. if (hasOpenBraceBefore) {
  2545. // Is a selector
  2546. buffer += character;
  2547. } else {
  2548. // Is a declaration
  2549. name = buffer.trim();
  2550. buffer = "";
  2551. state = "before-value";
  2552. }
  2553. } else {
  2554. buffer += character;
  2555. }
  2556. break;
  2557. case "(":
  2558. if (state === 'value') {
  2559. // ie css expression mode
  2560. if (buffer.trim() === 'expression') {
  2561. var info = (new CSSOM.CSSValueExpression(token, i)).parse();
  2562. if (info.error) {
  2563. parseError(info.error);
  2564. } else {
  2565. buffer += info.expression;
  2566. i = info.idx;
  2567. }
  2568. } else {
  2569. state = 'value-parenthesis';
  2570. //always ensure this is reset to 1 on transition
  2571. //from value to value-parenthesis
  2572. valueParenthesisDepth = 1;
  2573. buffer += character;
  2574. }
  2575. } else if (state === 'value-parenthesis') {
  2576. valueParenthesisDepth++;
  2577. buffer += character;
  2578. } else {
  2579. buffer += character;
  2580. }
  2581. break;
  2582. case ")":
  2583. if (state === 'value-parenthesis') {
  2584. valueParenthesisDepth--;
  2585. if (valueParenthesisDepth === 0) state = 'value';
  2586. }
  2587. buffer += character;
  2588. break;
  2589. case "!":
  2590. if (state === "value" && token.indexOf("!important", i) === i) {
  2591. priority = "important";
  2592. i += "important".length;
  2593. } else {
  2594. buffer += character;
  2595. }
  2596. break;
  2597. case ";":
  2598. switch (state) {
  2599. case "before-value":
  2600. case "before-name":
  2601. parseError("Unexpected ;");
  2602. buffer = "";
  2603. state = "before-name";
  2604. break;
  2605. case "value":
  2606. styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
  2607. priority = "";
  2608. buffer = "";
  2609. state = "before-name";
  2610. break;
  2611. case "atRule":
  2612. buffer = "";
  2613. state = "before-selector";
  2614. break;
  2615. case "importRule":
  2616. var isValid = topScope.cssRules.length === 0 || topScope.cssRules.some(function (rule) {
  2617. return ['CSSImportRule', 'CSSLayerStatementRule'].indexOf(rule.constructor.name) !== -1
  2618. });
  2619. if (isValid) {
  2620. importRule = new CSSOM.CSSImportRule();
  2621. if (opts && opts.globalObject && opts.globalObject.CSSStyleSheet) {
  2622. importRule.__styleSheet = new opts.globalObject.CSSStyleSheet();
  2623. }
  2624. importRule.styleSheet.__constructed = false;
  2625. importRule.__parentStyleSheet = importRule.styleSheet.__parentStyleSheet = styleSheet;
  2626. importRule.parse(buffer + character);
  2627. topScope.cssRules.push(importRule);
  2628. }
  2629. buffer = "";
  2630. state = "before-selector";
  2631. break;
  2632. case "namespaceRule":
  2633. var isValid = topScope.cssRules.length === 0 || topScope.cssRules.every(function (rule) {
  2634. return ['CSSImportRule', 'CSSLayerStatementRule', 'CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1
  2635. });
  2636. if (isValid) {
  2637. try {
  2638. // Validate namespace syntax before creating the rule
  2639. var testNamespaceRule = new CSSOM.CSSNamespaceRule();
  2640. testNamespaceRule.parse(buffer + character);
  2641. namespaceRule = testNamespaceRule;
  2642. namespaceRule.__parentStyleSheet = styleSheet;
  2643. topScope.cssRules.push(namespaceRule);
  2644. // Track the namespace prefix for validation
  2645. if (namespaceRule.prefix) {
  2646. definedNamespacePrefixes[namespaceRule.prefix] = namespaceRule.namespaceURI;
  2647. }
  2648. } catch (e) {
  2649. parseError(e.message);
  2650. }
  2651. }
  2652. buffer = "";
  2653. state = "before-selector";
  2654. break;
  2655. case "layerBlock":
  2656. var nameListStr = buffer.trim().split(",").map(function (name) {
  2657. return name.trim();
  2658. });
  2659. var isInvalid = nameListStr.some(function (name) {
  2660. return name.trim().match(cssCustomIdentifierRegExp) === null;
  2661. });
  2662. // Check if there's a CSSStyleRule in the parent chain
  2663. var hasStyleRuleParent = false;
  2664. if (parentRule) {
  2665. var checkParent = parentRule;
  2666. while (checkParent) {
  2667. if (checkParent.constructor.name === "CSSStyleRule") {
  2668. hasStyleRuleParent = true;
  2669. break;
  2670. }
  2671. checkParent = checkParent.__parentRule;
  2672. }
  2673. }
  2674. if (!isInvalid && !hasStyleRuleParent) {
  2675. layerStatementRule = new CSSOM.CSSLayerStatementRule();
  2676. layerStatementRule.__parentStyleSheet = styleSheet;
  2677. layerStatementRule.__starts = layerBlockRule.__starts;
  2678. layerStatementRule.__ends = i;
  2679. layerStatementRule.nameList = nameListStr;
  2680. // Add to parent rule if nested, otherwise to top scope
  2681. if (parentRule) {
  2682. layerStatementRule.__parentRule = parentRule;
  2683. parentRule.cssRules.push(layerStatementRule);
  2684. } else {
  2685. topScope.cssRules.push(layerStatementRule);
  2686. }
  2687. }
  2688. buffer = "";
  2689. state = "before-selector";
  2690. break;
  2691. default:
  2692. buffer += character;
  2693. break;
  2694. }
  2695. break;
  2696. case "}":
  2697. if (state === "counterStyleBlock") {
  2698. // FIXME : Implement missing properties on CSSCounterStyleRule interface and update parse method
  2699. // For now it's just assigning entire rule text
  2700. if (counterStyleRule.name) {
  2701. // Only process if name was set (valid)
  2702. counterStyleRule.parse("@counter-style " + counterStyleRule.name + " { " + buffer + " }");
  2703. counterStyleRule.__ends = i + 1;
  2704. // Add to parent's cssRules
  2705. if (counterStyleRule.__parentRule) {
  2706. counterStyleRule.__parentRule.cssRules.push(counterStyleRule);
  2707. } else {
  2708. topScope.cssRules.push(counterStyleRule);
  2709. }
  2710. }
  2711. // Restore currentScope to parent after closing this rule
  2712. if (counterStyleRule.__parentRule) {
  2713. currentScope = counterStyleRule.__parentRule;
  2714. }
  2715. styleRule = null;
  2716. buffer = "";
  2717. state = "before-selector";
  2718. break;
  2719. }
  2720. if (state === "propertyBlock") {
  2721. // Only process if name was set (valid)
  2722. if (propertyRule.__name) {
  2723. var parseSuccess = propertyRule.parse("@property " + propertyRule.__name + " { " + buffer + " }");
  2724. // Only add the rule if parse was successful (syntax, inherits, and initial-value validation passed)
  2725. if (parseSuccess) {
  2726. propertyRule.__ends = i + 1;
  2727. // Add to parent's cssRules
  2728. if (propertyRule.__parentRule) {
  2729. propertyRule.__parentRule.cssRules.push(propertyRule);
  2730. } else {
  2731. topScope.cssRules.push(propertyRule);
  2732. }
  2733. }
  2734. }
  2735. // Restore currentScope to parent after closing this rule
  2736. if (propertyRule.__parentRule) {
  2737. currentScope = propertyRule.__parentRule;
  2738. }
  2739. styleRule = null;
  2740. buffer = "";
  2741. state = "before-selector";
  2742. break;
  2743. }
  2744. switch (state) {
  2745. case "value":
  2746. styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
  2747. priority = "";
  2748. /* falls through */
  2749. case "before-value":
  2750. case "before-name":
  2751. case "name":
  2752. styleRule.__ends = i + 1;
  2753. if (parentRule === styleRule) {
  2754. parentRule = ancestorRules.pop()
  2755. }
  2756. if (parentRule) {
  2757. styleRule.__parentRule = parentRule;
  2758. }
  2759. styleRule.__parentStyleSheet = styleSheet;
  2760. if (currentScope === styleRule) {
  2761. currentScope = parentRule || topScope;
  2762. }
  2763. if (styleRule.constructor.name === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) {
  2764. if (styleRule === nestedSelectorRule) {
  2765. nestedSelectorRule = null;
  2766. }
  2767. parseError('Invalid CSSStyleRule (selectorText = "' + styleRule.selectorText + '")', styleRule.parentRule !== null);
  2768. } else {
  2769. if (styleRule.parentRule) {
  2770. styleRule.parentRule.cssRules.push(styleRule);
  2771. } else {
  2772. currentScope.cssRules.push(styleRule);
  2773. }
  2774. }
  2775. buffer = "";
  2776. if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
  2777. state = "keyframeRule-begin";
  2778. } else {
  2779. state = "before-selector";
  2780. }
  2781. if (styleRule.constructor.name === "CSSNestedDeclarations") {
  2782. if (currentScope !== topScope) {
  2783. // Only set nestedSelectorRule if currentScope is CSSStyleRule or CSSScopeRule
  2784. // Not for other grouping rules like @media/@supports
  2785. if (currentScope.constructor.name === "CSSStyleRule" || currentScope.constructor.name === "CSSScopeRule") {
  2786. nestedSelectorRule = currentScope;
  2787. }
  2788. }
  2789. styleRule = null;
  2790. } else {
  2791. // Update nestedSelectorRule when closing a CSSStyleRule
  2792. if (styleRule === nestedSelectorRule) {
  2793. var selector = styleRule.selectorText && styleRule.selectorText.trim();
  2794. // Check if this is proper nesting (&.class, &:pseudo) vs prepended & (& :is, & .class with space)
  2795. // Prepended & has pattern "& X" where X starts with : or .
  2796. var isPrependedAmpersand = selector && selector.match(prependedAmpersandRegExp);
  2797. // Check if parent is a grouping rule that can contain nested selectors
  2798. var isGroupingRule = currentScope && currentScope instanceof CSSOM.CSSGroupingRule;
  2799. if (!isPrependedAmpersand && isGroupingRule) {
  2800. // Proper nesting - set nestedSelectorRule to parent for more nested selectors
  2801. // But only if it's a CSSStyleRule or CSSScopeRule, not other grouping rules like @media
  2802. if (currentScope.constructor.name === "CSSStyleRule" || currentScope.constructor.name === "CSSScopeRule") {
  2803. nestedSelectorRule = currentScope;
  2804. }
  2805. // If currentScope is another type of grouping rule (like @media), keep nestedSelectorRule unchanged
  2806. } else {
  2807. // Prepended & or not nested in grouping rule - reset to prevent CSSNestedDeclarations
  2808. nestedSelectorRule = null;
  2809. }
  2810. } else if (nestedSelectorRule && currentScope instanceof CSSOM.CSSGroupingRule) {
  2811. // When closing a nested rule that's not the nestedSelectorRule itself,
  2812. // maintain nestedSelectorRule if we're still inside a grouping rule
  2813. // This ensures declarations after nested selectors inside @media/@supports etc. work correctly
  2814. }
  2815. styleRule = null;
  2816. break;
  2817. }
  2818. case "keyframeRule-begin":
  2819. case "before-selector":
  2820. case "selector":
  2821. // End of media/supports/document rule.
  2822. if (!parentRule) {
  2823. parseError("Unexpected }");
  2824. var hasPreviousStyleRule = currentScope.cssRules.length && currentScope.cssRules[currentScope.cssRules.length - 1].constructor.name === "CSSStyleRule";
  2825. if (hasPreviousStyleRule) {
  2826. i = ignoreBalancedBlock(i, token.slice(i), 1);
  2827. }
  2828. break;
  2829. }
  2830. // Find the actual parent rule by popping from ancestor stack
  2831. while (ancestorRules.length > 0) {
  2832. parentRule = ancestorRules.pop();
  2833. // Skip if we popped the current scope itself (happens because we push both rule and parent)
  2834. if (parentRule === currentScope) {
  2835. continue;
  2836. }
  2837. // Only process valid grouping rules
  2838. if (!(parentRule instanceof CSSOM.CSSGroupingRule && (parentRule.constructor.name !== 'CSSStyleRule' || parentRule.__parentRule))) {
  2839. continue;
  2840. }
  2841. // Determine if we're closing a special nested selector context
  2842. var isClosingNestedSelectorContext = nestedSelectorRule &&
  2843. (currentScope === nestedSelectorRule || nestedSelectorRule.__parentRule === currentScope);
  2844. if (isClosingNestedSelectorContext) {
  2845. // Closing the nestedSelectorRule or its direct container
  2846. if (nestedSelectorRule.parentRule) {
  2847. // Add nestedSelectorRule to its parent and update scope
  2848. prevScope = nestedSelectorRule;
  2849. currentScope = nestedSelectorRule.parentRule;
  2850. // Use object lookup instead of O(n) indexOf
  2851. var scopeId = getRuleId(prevScope);
  2852. if (!addedToCurrentScope[scopeId]) {
  2853. currentScope.cssRules.push(prevScope);
  2854. addedToCurrentScope[scopeId] = true;
  2855. }
  2856. nestedSelectorRule = currentScope;
  2857. // Stop here to preserve context for sibling selectors
  2858. break;
  2859. } else {
  2860. // Top-level CSSStyleRule with nested grouping rule
  2861. prevScope = currentScope;
  2862. var actualParent = ancestorRules.length > 0 ? ancestorRules[ancestorRules.length - 1] : nestedSelectorRule;
  2863. if (actualParent !== prevScope) {
  2864. actualParent.cssRules.push(prevScope);
  2865. }
  2866. currentScope = actualParent;
  2867. parentRule = actualParent;
  2868. break;
  2869. }
  2870. } else {
  2871. // Regular case: add currentScope to parentRule
  2872. prevScope = currentScope;
  2873. if (parentRule !== prevScope) {
  2874. parentRule.cssRules.push(prevScope);
  2875. }
  2876. break;
  2877. }
  2878. }
  2879. // If currentScope has a __parentRule and wasn't added yet, add it
  2880. if (ancestorRules.length === 0 && currentScope.__parentRule && currentScope.__parentRule.cssRules) {
  2881. // Use object lookup instead of O(n) findIndex
  2882. var parentId = getRuleId(currentScope);
  2883. if (!addedToParent[parentId]) {
  2884. currentScope.__parentRule.cssRules.push(currentScope);
  2885. addedToParent[parentId] = true;
  2886. }
  2887. }
  2888. // Only handle top-level rule closing if we processed all ancestors
  2889. if (ancestorRules.length === 0 && currentScope.parentRule == null) {
  2890. currentScope.__ends = i + 1;
  2891. // Use object lookup instead of O(n) findIndex
  2892. var topId = getRuleId(currentScope);
  2893. if (currentScope !== topScope && !addedToTopScope[topId]) {
  2894. topScope.cssRules.push(currentScope);
  2895. addedToTopScope[topId] = true;
  2896. }
  2897. currentScope = topScope;
  2898. if (nestedSelectorRule === parentRule) {
  2899. // Check if this selector is really starting inside another selector
  2900. var nestedSelectorTokenToCurrentSelectorToken = token.slice(nestedSelectorRule.__starts, i + 1);
  2901. var openingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(openBraceGlobalRegExp);
  2902. var closingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(closeBraceGlobalRegExp);
  2903. var openingBraceLen = openingBraceMatch && openingBraceMatch.length;
  2904. var closingBraceLen = closingBraceMatch && closingBraceMatch.length;
  2905. if (openingBraceLen === closingBraceLen) {
  2906. // If the number of opening and closing braces are equal, we can assume that the new selector is starting outside the nestedSelectorRule
  2907. nestedSelectorRule.__ends = i + 1;
  2908. nestedSelectorRule = null;
  2909. parentRule = null;
  2910. }
  2911. } else {
  2912. parentRule = null;
  2913. }
  2914. } else {
  2915. currentScope = parentRule;
  2916. }
  2917. buffer = "";
  2918. state = "before-selector";
  2919. break;
  2920. }
  2921. break;
  2922. default:
  2923. switch (state) {
  2924. case "before-selector":
  2925. state = "selector";
  2926. if ((styleRule || scopeRule) && parentRule) {
  2927. // Assuming it's a declaration inside Nested Selector OR a Nested Declaration
  2928. // If Declaration inside Nested Selector let's keep the same styleRule
  2929. if (!isSelectorStartChar(character) && !isWhitespaceChar(character) && parentRule instanceof CSSOM.CSSGroupingRule) {
  2930. // parentRule.__parentRule = styleRule;
  2931. state = "before-name";
  2932. if (styleRule !== parentRule) {
  2933. styleRule = new CSSOM.CSSNestedDeclarations();
  2934. styleRule.__starts = i;
  2935. }
  2936. }
  2937. } else if (nestedSelectorRule && parentRule && parentRule instanceof CSSOM.CSSGroupingRule) {
  2938. if (isSelectorStartChar(character)) {
  2939. // If starting with a selector character, create CSSStyleRule instead of CSSNestedDeclarations
  2940. styleRule = new CSSOM.CSSStyleRule();
  2941. styleRule.__starts = i;
  2942. } else if (!isWhitespaceChar(character)) {
  2943. // Starting a declaration (not whitespace, not a selector)
  2944. state = "before-name";
  2945. // Check if we should create CSSNestedDeclarations
  2946. // This happens if: parent has cssRules OR nestedSelectorRule exists (indicating CSSStyleRule in hierarchy)
  2947. if (parentRule.cssRules.length || nestedSelectorRule) {
  2948. currentScope = parentRule;
  2949. // Only set nestedSelectorRule if parentRule is CSSStyleRule or CSSScopeRule
  2950. if (parentRule.constructor.name === "CSSStyleRule" || parentRule.constructor.name === "CSSScopeRule") {
  2951. nestedSelectorRule = parentRule;
  2952. }
  2953. styleRule = new CSSOM.CSSNestedDeclarations();
  2954. styleRule.__starts = i;
  2955. } else {
  2956. if (parentRule.constructor.name === "CSSStyleRule") {
  2957. styleRule = parentRule;
  2958. } else {
  2959. styleRule = new CSSOM.CSSStyleRule();
  2960. styleRule.__starts = i;
  2961. }
  2962. }
  2963. }
  2964. }
  2965. break;
  2966. case "before-name":
  2967. state = "name";
  2968. break;
  2969. case "before-value":
  2970. state = "value";
  2971. break;
  2972. case "importRule-begin":
  2973. state = "importRule";
  2974. break;
  2975. case "namespaceRule-begin":
  2976. state = "namespaceRule";
  2977. break;
  2978. }
  2979. buffer += character;
  2980. break;
  2981. }
  2982. // Auto-close all unclosed nested structures
  2983. // Check AFTER processing the character, at the ORIGINAL ending index
  2984. // Only add closing braces if CSS is incomplete (not at top scope)
  2985. if (i === initialEndingIndex && (currentScope !== topScope || ancestorRules.length > 0)) {
  2986. var needsClosing = ancestorRules.length;
  2987. if (currentScope !== topScope && ancestorRules.indexOf(currentScope) === -1) {
  2988. needsClosing += 1;
  2989. }
  2990. // Add closing braces for all unclosed structures
  2991. for (var closeIdx = 0; closeIdx < needsClosing; closeIdx++) {
  2992. token += "}";
  2993. endingIndex += 1;
  2994. }
  2995. }
  2996. }
  2997. if (buffer.trim() !== "") {
  2998. parseError("Unexpected end of input");
  2999. }
  3000. return styleSheet;
  3001. };
  3002. //.CommonJS
  3003. exports.parse = CSSOM.parse;
  3004. // The following modules cannot be included sooner due to the mutual dependency with parse.js
  3005. CSSOM.CSSStyleSheet = require("./CSSStyleSheet").CSSStyleSheet;
  3006. CSSOM.CSSStyleRule = require("./CSSStyleRule").CSSStyleRule;
  3007. CSSOM.CSSNestedDeclarations = require("./CSSNestedDeclarations").CSSNestedDeclarations;
  3008. CSSOM.CSSImportRule = require("./CSSImportRule").CSSImportRule;
  3009. CSSOM.CSSNamespaceRule = require("./CSSNamespaceRule").CSSNamespaceRule;
  3010. CSSOM.CSSGroupingRule = require("./CSSGroupingRule").CSSGroupingRule;
  3011. CSSOM.CSSMediaRule = require("./CSSMediaRule").CSSMediaRule;
  3012. CSSOM.CSSCounterStyleRule = require("./CSSCounterStyleRule").CSSCounterStyleRule;
  3013. CSSOM.CSSPropertyRule = require("./CSSPropertyRule").CSSPropertyRule;
  3014. CSSOM.CSSContainerRule = require("./CSSContainerRule").CSSContainerRule;
  3015. CSSOM.CSSConditionRule = require("./CSSConditionRule").CSSConditionRule;
  3016. CSSOM.CSSSupportsRule = require("./CSSSupportsRule").CSSSupportsRule;
  3017. CSSOM.CSSFontFaceRule = require("./CSSFontFaceRule").CSSFontFaceRule;
  3018. CSSOM.CSSHostRule = require("./CSSHostRule").CSSHostRule;
  3019. CSSOM.CSSStartingStyleRule = require("./CSSStartingStyleRule").CSSStartingStyleRule;
  3020. CSSOM.CSSStyleDeclaration = require('./CSSStyleDeclaration').CSSStyleDeclaration;
  3021. CSSOM.CSSKeyframeRule = require('./CSSKeyframeRule').CSSKeyframeRule;
  3022. CSSOM.CSSKeyframesRule = require('./CSSKeyframesRule').CSSKeyframesRule;
  3023. CSSOM.CSSValueExpression = require('./CSSValueExpression').CSSValueExpression;
  3024. CSSOM.CSSDocumentRule = require('./CSSDocumentRule').CSSDocumentRule;
  3025. CSSOM.CSSScopeRule = require('./CSSScopeRule').CSSScopeRule;
  3026. CSSOM.CSSLayerBlockRule = require("./CSSLayerBlockRule").CSSLayerBlockRule;
  3027. CSSOM.CSSLayerStatementRule = require("./CSSLayerStatementRule").CSSLayerStatementRule;
  3028. CSSOM.CSSPageRule = require("./CSSPageRule").CSSPageRule;
  3029. // Use cssstyle if available
  3030. require("./cssstyleTryCatchBlock");
  3031. ///CommonJS