CSSOM.js 199 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611
  1. var CSSOM = {
  2. /**
  3. * Creates and configures a new CSSOM instance with the specified options.
  4. *
  5. * @param {Object} opts - Configuration options for the CSSOM instance
  6. * @param {Object} [opts.globalObject] - Optional global object to be assigned to CSSOM objects prototype
  7. * @returns {Object} A new CSSOM instance with the applied configuration
  8. * @description
  9. * This method creates a new instance of CSSOM and optionally
  10. * configures CSSStyleSheet with a global object reference. When a globalObject is provided
  11. * and CSSStyleSheet exists on the instance, it creates a new CSSStyleSheet constructor
  12. * using a factory function and assigns the globalObject to its prototype's __globalObject property.
  13. */
  14. setup: function (opts) {
  15. var instance = Object.create(this);
  16. if (opts.globalObject) {
  17. if (instance.CSSStyleSheet) {
  18. var factoryCSSStyleSheet = createFunctionFactory(instance.CSSStyleSheet);
  19. var CSSStyleSheet = factoryCSSStyleSheet();
  20. CSSStyleSheet.prototype.__globalObject = opts.globalObject;
  21. instance.CSSStyleSheet = CSSStyleSheet;
  22. }
  23. }
  24. return instance;
  25. }
  26. };
  27. function createFunctionFactory(fn) {
  28. return function() {
  29. // Create a new function that delegates to the original
  30. var newFn = function() {
  31. return fn.apply(this, arguments);
  32. };
  33. // Copy prototype chain
  34. Object.setPrototypeOf(newFn, Object.getPrototypeOf(fn));
  35. // Copy own properties
  36. for (var key in fn) {
  37. if (Object.prototype.hasOwnProperty.call(fn, key)) {
  38. newFn[key] = fn[key];
  39. }
  40. }
  41. // Clone the .prototype object for constructor-like behavior
  42. if (fn.prototype) {
  43. newFn.prototype = Object.create(fn.prototype);
  44. }
  45. return newFn;
  46. };
  47. }
  48. // Utility functions for CSSOM error handling
  49. /**
  50. * Gets the appropriate error constructor from the global object context.
  51. * Tries to find the error constructor from parentStyleSheet.__globalObject,
  52. * then from __globalObject, then falls back to the native constructor.
  53. *
  54. * @param {Object} context - The CSSOM object (rule, stylesheet, etc.)
  55. * @param {string} errorType - The error type ('TypeError', 'RangeError', 'DOMException', etc.)
  56. * @return {Function} The error constructor
  57. */
  58. function getErrorConstructor(context, errorType) {
  59. // Try parentStyleSheet.__globalObject first
  60. if (context.parentStyleSheet && context.parentStyleSheet.__globalObject && context.parentStyleSheet.__globalObject[errorType]) {
  61. return context.parentStyleSheet.__globalObject[errorType];
  62. }
  63. // Try __parentStyleSheet (alternative naming)
  64. if (context.__parentStyleSheet && context.__parentStyleSheet.__globalObject && context.__parentStyleSheet.__globalObject[errorType]) {
  65. return context.__parentStyleSheet.__globalObject[errorType];
  66. }
  67. // Try __globalObject on the context itself
  68. if (context.__globalObject && context.__globalObject[errorType]) {
  69. return context.__globalObject[errorType];
  70. }
  71. // Fall back to native constructor
  72. return (typeof global !== 'undefined' && global[errorType]) ||
  73. (typeof window !== 'undefined' && window[errorType]) ||
  74. eval(errorType);
  75. }
  76. /**
  77. * Creates an appropriate error with context-aware constructor.
  78. *
  79. * @param {Object} context - The CSSOM object (rule, stylesheet, etc.)
  80. * @param {string} errorType - The error type ('TypeError', 'RangeError', 'DOMException', etc.)
  81. * @param {string} message - The error message
  82. * @param {string} [name] - Optional name for DOMException
  83. */
  84. function createError(context, errorType, message, name) {
  85. var ErrorConstructor = getErrorConstructor(context, errorType);
  86. return new ErrorConstructor(message, name);
  87. }
  88. /**
  89. * Creates and throws an appropriate error with context-aware constructor.
  90. *
  91. * @param {Object} context - The CSSOM object (rule, stylesheet, etc.)
  92. * @param {string} errorType - The error type ('TypeError', 'RangeError', 'DOMException', etc.)
  93. * @param {string} message - The error message
  94. * @param {string} [name] - Optional name for DOMException
  95. */
  96. function throwError(context, errorType, message, name) {
  97. throw createError(context, errorType, message, name);
  98. }
  99. /**
  100. * Throws a TypeError for missing required arguments.
  101. *
  102. * @param {Object} context - The CSSOM object
  103. * @param {string} methodName - The method name (e.g., 'appendRule')
  104. * @param {string} objectName - The object name (e.g., 'CSSKeyframesRule')
  105. * @param {number} [required=1] - Number of required arguments
  106. * @param {number} [provided=0] - Number of provided arguments
  107. */
  108. function throwMissingArguments(context, methodName, objectName, required, provided) {
  109. required = required || 1;
  110. provided = provided || 0;
  111. var message = "Failed to execute '" + methodName + "' on '" + objectName + "': " +
  112. required + " argument" + (required > 1 ? "s" : "") + " required, but only " +
  113. provided + " present.";
  114. throwError(context, 'TypeError', message);
  115. }
  116. /**
  117. * Throws a DOMException for parse errors.
  118. *
  119. * @param {Object} context - The CSSOM object
  120. * @param {string} methodName - The method name
  121. * @param {string} objectName - The object name
  122. * @param {string} rule - The rule that failed to parse
  123. * @param {string} [name='SyntaxError'] - The DOMException name
  124. */
  125. function throwParseError(context, methodName, objectName, rule, name) {
  126. var message = "Failed to execute '" + methodName + "' on '" + objectName + "': " +
  127. "Failed to parse the rule '" + rule + "'.";
  128. throwError(context, 'DOMException', message, name || 'SyntaxError');
  129. }
  130. /**
  131. * Throws a DOMException for index errors.
  132. *
  133. * @param {Object} context - The CSSOM object
  134. * @param {string} methodName - The method name
  135. * @param {string} objectName - The object name
  136. * @param {number} index - The invalid index
  137. * @param {number} maxIndex - The maximum valid index
  138. * @param {string} [name='IndexSizeError'] - The DOMException name
  139. */
  140. function throwIndexError(context, methodName, objectName, index, maxIndex, name) {
  141. var message = "Failed to execute '" + methodName + "' on '" + objectName + "': " +
  142. "The index provided (" + index + ") is larger than the maximum index (" + maxIndex + ").";
  143. throwError(context, 'DOMException', message, name || 'IndexSizeError');
  144. }
  145. var errorUtils = {
  146. createError: createError,
  147. getErrorConstructor: getErrorConstructor,
  148. throwError: throwError,
  149. throwMissingArguments: throwMissingArguments,
  150. throwParseError: throwParseError,
  151. throwIndexError: throwIndexError
  152. };
  153. // Shared regex patterns for CSS parsing and validation
  154. // These patterns are compiled once and reused across multiple files for better performance
  155. // Regex patterns for CSS parsing
  156. var atKeyframesRegExp = /@(-(?:\w+-)+)?keyframes/g; // Match @keyframes and vendor-prefixed @keyframes
  157. var beforeRulePortionRegExp = /{(?!.*{)|}(?!.*})|;(?!.*;)|\*\/(?!.*\*\/)/g; // Match the closest allowed character (a opening or closing brace, a semicolon or a comment ending) before the rule
  158. var beforeRuleValidationRegExp = /^[\s{};]*(\*\/\s*)?$/; // Match that the portion before the rule is empty or contains only whitespace, semicolons, opening/closing braces, and optionally a comment ending (*/) followed by whitespace
  159. var forwardRuleValidationRegExp = /(?:\s|\/\*|\{|\()/; // Match that the rule is followed by any whitespace, a opening comment, a condition opening parenthesis or a opening brace
  160. var forwardImportRuleValidationRegExp = /(?:\s|\/\*|'|")/; // Match that the rule is followed by any whitespace, an opening comment, a single quote or double quote
  161. var forwardRuleClosingBraceRegExp = /{[^{}]*}|}/; // Finds the next closing brace of a rule block
  162. var forwardRuleSemicolonAndOpeningBraceRegExp = /^.*?({|;)/; // Finds the next semicolon or opening brace after the at-rule
  163. // Regex patterns for CSS selector validation and parsing
  164. var cssCustomIdentifierRegExp = /^(-?[_a-zA-Z]+(\.[_a-zA-Z]+)*[_a-zA-Z0-9-]*)$/; // Validates a css custom identifier
  165. var startsWithCombinatorRegExp = /^\s*[>+~]/; // Checks if a selector starts with a CSS combinator (>, +, ~)
  166. /**
  167. * Parse `@page` selectorText for page name and pseudo-pages
  168. * Valid formats:
  169. * - (empty - no name, no pseudo-page)
  170. * - `:left`, `:right`, `:first`, `:blank` (pseudo-page only)
  171. * - `named` (named page only)
  172. * - `named:first` (named page with single pseudo-page)
  173. * - `named:first:left` (named page with multiple pseudo-pages)
  174. */
  175. var atPageRuleSelectorRegExp = /^([^\s:]+)?((?::\w+)*)$/; // Validates @page rule selectors
  176. // Regex patterns for CSSImportRule parsing
  177. var layerRegExp = /layer\(([^)]*)\)/; // Matches layer() function in @import
  178. var layerRuleNameRegExp = /^(-?[_a-zA-Z]+(\.[_a-zA-Z]+)*[_a-zA-Z0-9-]*)$/; // Validates layer name (same as custom identifier)
  179. var doubleOrMoreSpacesRegExp = /\s{2,}/g; // Matches two or more consecutive whitespace characters
  180. // Regex patterns for CSS escape sequences and identifiers
  181. var startsWithHexEscapeRegExp = /^\\[0-9a-fA-F]/; // Checks if escape sequence starts with hex escape
  182. var identStartCharRegExp = /[a-zA-Z_\u00A0-\uFFFF]/; // Valid identifier start character
  183. var identCharRegExp = /^[a-zA-Z0-9_\-\u00A0-\uFFFF\\]/; // Valid identifier character
  184. var specialCharsNeedEscapeRegExp = /[!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~\s]/; // Characters that need escaping
  185. var combinatorOrSeparatorRegExp = /[\s>+~,()]/; // Selector boundaries and combinators
  186. var afterHexEscapeSeparatorRegExp = /[\s>+~,(){}\[\]]/; // Characters that separate after hex escape
  187. var trailingSpaceSeparatorRegExp = /[\s>+~,(){}]/; // Characters that allow trailing space
  188. var endsWithHexEscapeRegExp = /\\[0-9a-fA-F]{1,6}\s+$/; // Matches selector ending with hex escape + space(s)
  189. /**
  190. * Regular expression to detect invalid characters in the value portion of a CSS style declaration.
  191. *
  192. * This regex matches a colon (:) that is not inside parentheses and not inside single or double quotes.
  193. * It is used to ensure that the value part of a CSS property does not contain unexpected colons,
  194. * which would indicate a malformed declaration (e.g., "color: foo:bar;" is invalid).
  195. *
  196. * The negative lookahead `(?![^(]*\))` ensures that the colon is not followed by a closing
  197. * parenthesis without encountering an opening parenthesis, effectively ignoring colons inside
  198. * function-like values (e.g., `url(data:image/png;base64,...)`).
  199. *
  200. * The lookahead `(?=(?:[^'"]|'[^']*'|"[^"]*")*$)` ensures that the colon is not inside single or double quotes,
  201. * allowing colons within quoted strings (e.g., `content: ":";` or `background: url("foo:bar.png");`).
  202. *
  203. * Example:
  204. * - `color: red;` // valid, does not match
  205. * - `background: url(data:image/png;base64,...);` // valid, does not match
  206. * - `content: ':';` // valid, does not match
  207. * - `color: foo:bar;` // invalid, matches
  208. */
  209. var basicStylePropertyValueValidationRegExp = /:(?![^(]*\))(?=(?:[^'"]|'[^']*'|"[^"]*")*$)/;
  210. // Attribute selector pattern: matches attribute-name operator value
  211. // Operators: =, ~=, |=, ^=, $=, *=
  212. // Rewritten to avoid ReDoS by using greedy match and trimming in JavaScript
  213. var attributeSelectorContentRegExp = /^([^\s=~|^$*]+)\s*(~=|\|=|\^=|\$=|\*=|=)\s*(.+)$/;
  214. // Selector validation patterns
  215. var pseudoElementRegExp = /::[a-zA-Z][\w-]*|:(before|after|first-line|first-letter)(?![a-zA-Z0-9_-])/; // Matches pseudo-elements
  216. var invalidCombinatorLtGtRegExp = /<>/; // Invalid <> combinator
  217. var invalidCombinatorDoubleGtRegExp = />>/; // Invalid >> combinator
  218. var consecutiveCombinatorsRegExp = /[>+~]\s*[>+~]/; // Invalid consecutive combinators
  219. var invalidSlottedRegExp = /(?:^|[\s>+~,\[])slotted\s*\(/i; // Invalid slotted() without ::
  220. var invalidPartRegExp = /(?:^|[\s>+~,\[])part\s*\(/i; // Invalid part() without ::
  221. var invalidCueRegExp = /(?:^|[\s>+~,\[])cue\s*\(/i; // Invalid cue() without ::
  222. var invalidCueRegionRegExp = /(?:^|[\s>+~,\[])cue-region\s*\(/i; // Invalid cue-region() without ::
  223. var invalidNestingPattern = /&(?![.\#\[:>\+~\s])[a-zA-Z]/; // Invalid & followed by type selector
  224. var emptyPseudoClassRegExp = /:(?:is|not|where|has)\(\s*\)/; // Empty pseudo-class like :is()
  225. var whitespaceNormalizationRegExp = /(['"])(?:\\.|[^\\])*?\1|(\r\n|\r|\n)/g; // Normalize newlines outside quotes
  226. var newlineRemovalRegExp = /\n/g; // Remove all newlines
  227. var whitespaceAndDotRegExp = /[\s.]/; // Matches whitespace or dot
  228. var declarationOrOpenBraceRegExp = /[{;}]/; // Matches declaration separator or open brace
  229. var ampersandRegExp = /&/; // Matches nesting selector
  230. var hexEscapeSequenceRegExp = /^([0-9a-fA-F]{1,6})[ \t\r\n\f]?/; // Matches hex escape sequence (1-6 hex digits optionally followed by whitespace)
  231. var attributeCaseFlagRegExp = /^(.+?)\s+([is])$/i; // Matches case-sensitivity flag at end of attribute value
  232. var prependedAmpersandRegExp = /^&\s+[:\\.]/; // Matches prepended ampersand pattern (& followed by space and : or .)
  233. var openBraceGlobalRegExp = /{/g; // Matches opening braces (global)
  234. var closeBraceGlobalRegExp = /}/g; // Matches closing braces (global)
  235. var scopePreludeSplitRegExp = /\s*\)\s*to\s+\(/; // Splits scope prelude by ") to ("
  236. var leadingWhitespaceRegExp = /^\s+/; // Matches leading whitespace (used to implement a ES5-compliant alternative to trimStart())
  237. var doubleQuoteRegExp = /"/g; // Match all double quotes (for escaping in attribute values)
  238. var backslashRegExp = /\\/g; // Match all backslashes (for escaping in attribute values)
  239. var regexPatterns = {
  240. // Parsing patterns
  241. atKeyframesRegExp: atKeyframesRegExp,
  242. beforeRulePortionRegExp: beforeRulePortionRegExp,
  243. beforeRuleValidationRegExp: beforeRuleValidationRegExp,
  244. forwardRuleValidationRegExp: forwardRuleValidationRegExp,
  245. forwardImportRuleValidationRegExp: forwardImportRuleValidationRegExp,
  246. forwardRuleClosingBraceRegExp: forwardRuleClosingBraceRegExp,
  247. forwardRuleSemicolonAndOpeningBraceRegExp: forwardRuleSemicolonAndOpeningBraceRegExp,
  248. // Selector validation patterns
  249. cssCustomIdentifierRegExp: cssCustomIdentifierRegExp,
  250. startsWithCombinatorRegExp: startsWithCombinatorRegExp,
  251. atPageRuleSelectorRegExp: atPageRuleSelectorRegExp,
  252. // Parsing patterns used in CSSImportRule
  253. layerRegExp: layerRegExp,
  254. layerRuleNameRegExp: layerRuleNameRegExp,
  255. doubleOrMoreSpacesRegExp: doubleOrMoreSpacesRegExp,
  256. // Escape sequence and identifier patterns
  257. startsWithHexEscapeRegExp: startsWithHexEscapeRegExp,
  258. identStartCharRegExp: identStartCharRegExp,
  259. identCharRegExp: identCharRegExp,
  260. specialCharsNeedEscapeRegExp: specialCharsNeedEscapeRegExp,
  261. combinatorOrSeparatorRegExp: combinatorOrSeparatorRegExp,
  262. afterHexEscapeSeparatorRegExp: afterHexEscapeSeparatorRegExp,
  263. trailingSpaceSeparatorRegExp: trailingSpaceSeparatorRegExp,
  264. endsWithHexEscapeRegExp: endsWithHexEscapeRegExp,
  265. // Basic style property value validation
  266. basicStylePropertyValueValidationRegExp: basicStylePropertyValueValidationRegExp,
  267. // Attribute selector patterns
  268. attributeSelectorContentRegExp: attributeSelectorContentRegExp,
  269. // Selector validation patterns
  270. pseudoElementRegExp: pseudoElementRegExp,
  271. invalidCombinatorLtGtRegExp: invalidCombinatorLtGtRegExp,
  272. invalidCombinatorDoubleGtRegExp: invalidCombinatorDoubleGtRegExp,
  273. consecutiveCombinatorsRegExp: consecutiveCombinatorsRegExp,
  274. invalidSlottedRegExp: invalidSlottedRegExp,
  275. invalidPartRegExp: invalidPartRegExp,
  276. invalidCueRegExp: invalidCueRegExp,
  277. invalidCueRegionRegExp: invalidCueRegionRegExp,
  278. invalidNestingPattern: invalidNestingPattern,
  279. emptyPseudoClassRegExp: emptyPseudoClassRegExp,
  280. whitespaceNormalizationRegExp: whitespaceNormalizationRegExp,
  281. newlineRemovalRegExp: newlineRemovalRegExp,
  282. whitespaceAndDotRegExp: whitespaceAndDotRegExp,
  283. declarationOrOpenBraceRegExp: declarationOrOpenBraceRegExp,
  284. ampersandRegExp: ampersandRegExp,
  285. hexEscapeSequenceRegExp: hexEscapeSequenceRegExp,
  286. attributeCaseFlagRegExp: attributeCaseFlagRegExp,
  287. prependedAmpersandRegExp: prependedAmpersandRegExp,
  288. openBraceGlobalRegExp: openBraceGlobalRegExp,
  289. closeBraceGlobalRegExp: closeBraceGlobalRegExp,
  290. scopePreludeSplitRegExp: scopePreludeSplitRegExp,
  291. leadingWhitespaceRegExp: leadingWhitespaceRegExp,
  292. doubleQuoteRegExp: doubleQuoteRegExp,
  293. backslashRegExp: backslashRegExp
  294. };
  295. /**
  296. * @constructor
  297. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration
  298. */
  299. CSSOM.CSSStyleDeclaration = function CSSStyleDeclaration(){
  300. this.length = 0;
  301. this.parentRule = null;
  302. // NON-STANDARD
  303. this._importants = {};
  304. };
  305. CSSOM.CSSStyleDeclaration.prototype = {
  306. constructor: CSSOM.CSSStyleDeclaration,
  307. /**
  308. *
  309. * @param {string} name
  310. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-getPropertyValue
  311. * @return {string} the value of the property if it has been explicitly set for this declaration block.
  312. * Returns the empty string if the property has not been set.
  313. */
  314. getPropertyValue: function(name) {
  315. return this[name] || "";
  316. },
  317. /**
  318. *
  319. * @param {string} name
  320. * @param {string} value
  321. * @param {string} [priority=null] "important" or null
  322. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-setProperty
  323. */
  324. setProperty: function(name, value, priority, parseErrorHandler)
  325. {
  326. // NOTE: Check viability to add a validation for css values or use a dependency like csstree-validator
  327. var basicStylePropertyValueValidationRegExp = regexPatterns.basicStylePropertyValueValidationRegExp
  328. if (basicStylePropertyValueValidationRegExp.test(value)) {
  329. parseErrorHandler && parseErrorHandler('Invalid CSSStyleDeclaration property (name = "' + name + '", value = "' + value + '")');
  330. } else if (this[name]) {
  331. // Property already exist. Overwrite it.
  332. var index = Array.prototype.indexOf.call(this, name);
  333. if (index < 0) {
  334. this[this.length] = name;
  335. this.length++;
  336. }
  337. // If the priority value of the incoming property is "important",
  338. // or the value of the existing property is not "important",
  339. // then remove the existing property and rewrite it.
  340. if (priority || !this._importants[name]) {
  341. this.removeProperty(name);
  342. this[this.length] = name;
  343. this.length++;
  344. this[name] = value + '';
  345. this._importants[name] = priority;
  346. }
  347. } else {
  348. // New property.
  349. this[this.length] = name;
  350. this.length++;
  351. this[name] = value + '';
  352. this._importants[name] = priority;
  353. }
  354. },
  355. /**
  356. *
  357. * @param {string} name
  358. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration-removeProperty
  359. * @return {string} the value of the property if it has been explicitly set for this declaration block.
  360. * Returns the empty string if the property has not been set or the property name does not correspond to a known CSS property.
  361. */
  362. removeProperty: function(name) {
  363. if (!(name in this)) {
  364. return "";
  365. }
  366. var index = Array.prototype.indexOf.call(this, name);
  367. if (index < 0) {
  368. return "";
  369. }
  370. var prevValue = this[name];
  371. this[name] = "";
  372. // That's what WebKit and Opera do
  373. Array.prototype.splice.call(this, index, 1);
  374. // That's what Firefox does
  375. //this[index] = ""
  376. return prevValue;
  377. },
  378. getPropertyCSSValue: function() {
  379. //FIXME
  380. },
  381. /**
  382. *
  383. * @param {String} name
  384. */
  385. getPropertyPriority: function(name) {
  386. return this._importants[name] || "";
  387. },
  388. /**
  389. * element.style.overflow = "auto"
  390. * element.style.getPropertyShorthand("overflow-x")
  391. * -> "overflow"
  392. */
  393. getPropertyShorthand: function() {
  394. //FIXME
  395. },
  396. isPropertyImplicit: function() {
  397. //FIXME
  398. },
  399. // Doesn't work in IE < 9
  400. get cssText(){
  401. var properties = [];
  402. for (var i=0, length=this.length; i < length; ++i) {
  403. var name = this[i];
  404. var value = this.getPropertyValue(name);
  405. var priority = this.getPropertyPriority(name);
  406. if (priority) {
  407. priority = " !" + priority;
  408. }
  409. properties[i] = name + ": " + value + priority + ";";
  410. }
  411. return properties.join(" ");
  412. },
  413. set cssText(text){
  414. var i, name;
  415. for (i = this.length; i--;) {
  416. name = this[i];
  417. this[name] = "";
  418. }
  419. Array.prototype.splice.call(this, 0, this.length);
  420. this._importants = {};
  421. var dummyRule = CSSOM.parse('#bogus{' + text + '}').cssRules[0].style;
  422. var length = dummyRule.length;
  423. for (i = 0; i < length; ++i) {
  424. name = dummyRule[i];
  425. this.setProperty(dummyRule[i], dummyRule.getPropertyValue(name), dummyRule.getPropertyPriority(name));
  426. }
  427. }
  428. };
  429. try {
  430. CSSOM.CSSStyleDeclaration = require("cssstyle").CSSStyleDeclaration;
  431. } catch (e) {
  432. // ignore
  433. }
  434. /**
  435. * @constructor
  436. * @see http://dev.w3.org/csswg/cssom/#the-cssrule-interface
  437. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSRule
  438. */
  439. CSSOM.CSSRule = function CSSRule() {
  440. this.__parentRule = null;
  441. this.__parentStyleSheet = null;
  442. };
  443. CSSOM.CSSRule.UNKNOWN_RULE = 0; // obsolete
  444. CSSOM.CSSRule.STYLE_RULE = 1;
  445. CSSOM.CSSRule.CHARSET_RULE = 2; // obsolete
  446. CSSOM.CSSRule.IMPORT_RULE = 3;
  447. CSSOM.CSSRule.MEDIA_RULE = 4;
  448. CSSOM.CSSRule.FONT_FACE_RULE = 5;
  449. CSSOM.CSSRule.PAGE_RULE = 6;
  450. CSSOM.CSSRule.KEYFRAMES_RULE = 7;
  451. CSSOM.CSSRule.KEYFRAME_RULE = 8;
  452. CSSOM.CSSRule.MARGIN_RULE = 9;
  453. CSSOM.CSSRule.NAMESPACE_RULE = 10;
  454. CSSOM.CSSRule.COUNTER_STYLE_RULE = 11;
  455. CSSOM.CSSRule.SUPPORTS_RULE = 12;
  456. CSSOM.CSSRule.DOCUMENT_RULE = 13;
  457. CSSOM.CSSRule.FONT_FEATURE_VALUES_RULE = 14;
  458. CSSOM.CSSRule.VIEWPORT_RULE = 15;
  459. CSSOM.CSSRule.REGION_STYLE_RULE = 16;
  460. CSSOM.CSSRule.CONTAINER_RULE = 17;
  461. CSSOM.CSSRule.LAYER_BLOCK_RULE = 18;
  462. CSSOM.CSSRule.STARTING_STYLE_RULE = 1002;
  463. Object.defineProperties(CSSOM.CSSRule.prototype, {
  464. constructor: { value: CSSOM.CSSRule },
  465. cssRule: {
  466. value: "",
  467. configurable: true,
  468. enumerable: true
  469. },
  470. cssText: {
  471. get: function() {
  472. // Default getter: subclasses should override this
  473. return "";
  474. },
  475. set: function(cssText) {
  476. return cssText;
  477. }
  478. },
  479. parentRule: {
  480. get: function() {
  481. return this.__parentRule
  482. }
  483. },
  484. parentStyleSheet: {
  485. get: function() {
  486. return this.__parentStyleSheet
  487. }
  488. },
  489. UNKNOWN_RULE: { value: 0, enumerable: true }, // obsolet
  490. STYLE_RULE: { value: 1, enumerable: true },
  491. CHARSET_RULE: { value: 2, enumerable: true }, // obsolet
  492. IMPORT_RULE: { value: 3, enumerable: true },
  493. MEDIA_RULE: { value: 4, enumerable: true },
  494. FONT_FACE_RULE: { value: 5, enumerable: true },
  495. PAGE_RULE: { value: 6, enumerable: true },
  496. KEYFRAMES_RULE: { value: 7, enumerable: true },
  497. KEYFRAME_RULE: { value: 8, enumerable: true },
  498. MARGIN_RULE: { value: 9, enumerable: true },
  499. NAMESPACE_RULE: { value: 10, enumerable: true },
  500. COUNTER_STYLE_RULE: { value: 11, enumerable: true },
  501. SUPPORTS_RULE: { value: 12, enumerable: true },
  502. DOCUMENT_RULE: { value: 13, enumerable: true },
  503. FONT_FEATURE_VALUES_RULE: { value: 14, enumerable: true },
  504. VIEWPORT_RULE: { value: 15, enumerable: true },
  505. REGION_STYLE_RULE: { value: 16, enumerable: true },
  506. CONTAINER_RULE: { value: 17, enumerable: true },
  507. LAYER_BLOCK_RULE: { value: 18, enumerable: true },
  508. STARTING_STYLE_RULE: { value: 1002, enumerable: true },
  509. });
  510. /**
  511. * @constructor
  512. * @see https://drafts.csswg.org/cssom/#the-cssrulelist-interface
  513. */
  514. CSSOM.CSSRuleList = function CSSRuleList(){
  515. var arr = new Array();
  516. Object.setPrototypeOf(arr, CSSOM.CSSRuleList.prototype);
  517. return arr;
  518. };
  519. CSSOM.CSSRuleList.prototype = Object.create(Array.prototype);
  520. CSSOM.CSSRuleList.prototype.constructor = CSSOM.CSSRuleList;
  521. CSSOM.CSSRuleList.prototype.item = function(index) {
  522. return this[index] || null;
  523. };
  524. /**
  525. * @constructor
  526. * @see https://drafts.csswg.org/css-nesting-1/
  527. */
  528. CSSOM.CSSNestedDeclarations = function CSSNestedDeclarations() {
  529. CSSOM.CSSRule.call(this);
  530. this.__style = new CSSOM.CSSStyleDeclaration();
  531. this.__style.parentRule = this;
  532. };
  533. CSSOM.CSSNestedDeclarations.prototype = Object.create(CSSOM.CSSRule.prototype);
  534. CSSOM.CSSNestedDeclarations.prototype.constructor = CSSOM.CSSNestedDeclarations;
  535. Object.setPrototypeOf(CSSOM.CSSNestedDeclarations, CSSOM.CSSRule);
  536. Object.defineProperty(CSSOM.CSSNestedDeclarations.prototype, "type", {
  537. value: 0,
  538. writable: false
  539. });
  540. Object.defineProperty(CSSOM.CSSNestedDeclarations.prototype, "style", {
  541. get: function() {
  542. return this.__style;
  543. },
  544. set: function(value) {
  545. if (typeof value === "string") {
  546. this.__style.cssText = value;
  547. } else {
  548. this.__style = value;
  549. }
  550. }
  551. });
  552. Object.defineProperty(CSSOM.CSSNestedDeclarations.prototype, "cssText", {
  553. get: function () {
  554. return this.style.cssText;
  555. }
  556. });
  557. /**
  558. * @constructor
  559. * @see https://drafts.csswg.org/cssom/#the-cssgroupingrule-interface
  560. */
  561. CSSOM.CSSGroupingRule = function CSSGroupingRule() {
  562. CSSOM.CSSRule.call(this);
  563. this.__cssRules = new CSSOM.CSSRuleList();
  564. };
  565. CSSOM.CSSGroupingRule.prototype = Object.create(CSSOM.CSSRule.prototype);
  566. CSSOM.CSSGroupingRule.prototype.constructor = CSSOM.CSSGroupingRule;
  567. Object.setPrototypeOf(CSSOM.CSSGroupingRule, CSSOM.CSSRule);
  568. Object.defineProperty(CSSOM.CSSGroupingRule.prototype, "cssRules", {
  569. get: function() {
  570. return this.__cssRules;
  571. }
  572. });
  573. /**
  574. * Used to insert a new CSS rule to a list of CSS rules.
  575. *
  576. * @example
  577. * cssGroupingRule.cssText
  578. * -> "body{margin:0;}"
  579. * cssGroupingRule.insertRule("img{border:none;}", 1)
  580. * -> 1
  581. * cssGroupingRule.cssText
  582. * -> "body{margin:0;}img{border:none;}"
  583. *
  584. * @param {string} rule
  585. * @param {number} [index]
  586. * @see https://www.w3.org/TR/cssom-1/#dom-cssgroupingrule-insertrule
  587. * @return {number} The index within the grouping rule's collection of the newly inserted rule.
  588. */
  589. CSSOM.CSSGroupingRule.prototype.insertRule = function insertRule(rule, index) {
  590. if (rule === undefined && index === undefined) {
  591. errorUtils.throwMissingArguments(this, 'insertRule', this.constructor.name);
  592. }
  593. if (index === void 0) {
  594. index = 0;
  595. }
  596. index = Number(index);
  597. if (index < 0) {
  598. index = 4294967296 + index;
  599. }
  600. if (index > this.cssRules.length) {
  601. errorUtils.throwIndexError(this, 'insertRule', this.constructor.name, index, this.cssRules.length);
  602. }
  603. var ruleToParse = processedRuleToParse = String(rule);
  604. ruleToParse = ruleToParse.trim().replace(/^\/\*[\s\S]*?\*\/\s*/, "");
  605. var isNestedSelector = this.constructor.name === "CSSStyleRule";
  606. if (isNestedSelector === false) {
  607. var currentRule = this;
  608. while (currentRule.parentRule) {
  609. currentRule = currentRule.parentRule;
  610. if (currentRule.constructor.name === "CSSStyleRule") {
  611. isNestedSelector = true;
  612. break;
  613. }
  614. }
  615. }
  616. if (isNestedSelector) {
  617. processedRuleToParse = 's { n { } ' + ruleToParse + '}';
  618. }
  619. var isScopeRule = this.constructor.name === "CSSScopeRule";
  620. if (isScopeRule) {
  621. if (isNestedSelector) {
  622. processedRuleToParse = 's { ' + '@scope {' + ruleToParse + '}}';
  623. } else {
  624. processedRuleToParse = '@scope {' + ruleToParse + '}';
  625. }
  626. }
  627. var parsedRules = new CSSOM.CSSRuleList();
  628. CSSOM.parse(processedRuleToParse, {
  629. styleSheet: this.parentStyleSheet,
  630. cssRules: parsedRules
  631. });
  632. if (isScopeRule) {
  633. if (isNestedSelector) {
  634. parsedRules = parsedRules[0].cssRules[0].cssRules;
  635. } else {
  636. parsedRules = parsedRules[0].cssRules
  637. }
  638. }
  639. if (isNestedSelector) {
  640. parsedRules = parsedRules[0].cssRules.slice(1);
  641. }
  642. if (parsedRules.length !== 1) {
  643. if (isNestedSelector && parsedRules.length === 0 && ruleToParse.indexOf('@font-face') === 0) {
  644. errorUtils.throwError(this, 'DOMException',
  645. "Failed to execute 'insertRule' on '" + this.constructor.name + "': " +
  646. "Only conditional nested group rules, style rules, @scope rules, @apply rules, and nested declaration rules may be nested.",
  647. 'HierarchyRequestError');
  648. } else {
  649. errorUtils.throwParseError(this, 'insertRule', this.constructor.name, ruleToParse, 'SyntaxError');
  650. }
  651. }
  652. var cssRule = parsedRules[0];
  653. if (cssRule.constructor.name === 'CSSNestedDeclarations' && cssRule.style.length === 0) {
  654. errorUtils.throwParseError(this, 'insertRule', this.constructor.name, ruleToParse, 'SyntaxError');
  655. }
  656. // Check for rules that cannot be inserted inside a CSSGroupingRule
  657. if (cssRule.constructor.name === 'CSSImportRule' || cssRule.constructor.name === 'CSSNamespaceRule') {
  658. var ruleKeyword = cssRule.constructor.name === 'CSSImportRule' ? '@import' : '@namespace';
  659. errorUtils.throwError(this, 'DOMException',
  660. "Failed to execute 'insertRule' on '" + this.constructor.name + "': " +
  661. "'" + ruleKeyword + "' rules cannot be inserted inside a group rule.",
  662. 'HierarchyRequestError');
  663. }
  664. // Check for CSSLayerStatementRule (@layer statement rules)
  665. if (cssRule.constructor.name === 'CSSLayerStatementRule') {
  666. errorUtils.throwParseError(this, 'insertRule', this.constructor.name, ruleToParse, 'SyntaxError');
  667. }
  668. cssRule.__parentRule = this;
  669. this.cssRules.splice(index, 0, cssRule);
  670. return index;
  671. };
  672. /**
  673. * Used to delete a rule from the grouping rule.
  674. *
  675. * cssGroupingRule.cssText
  676. * -> "img{border:none;}body{margin:0;}"
  677. * cssGroupingRule.deleteRule(0)
  678. * cssGroupingRule.cssText
  679. * -> "body{margin:0;}"
  680. *
  681. * @param {number} index within the grouping rule's rule list of the rule to remove.
  682. * @see https://www.w3.org/TR/cssom-1/#dom-cssgroupingrule-deleterule
  683. */
  684. CSSOM.CSSGroupingRule.prototype.deleteRule = function deleteRule(index) {
  685. if (index === undefined) {
  686. errorUtils.throwMissingArguments(this, 'deleteRule', this.constructor.name);
  687. }
  688. index = Number(index);
  689. if (index < 0) {
  690. index = 4294967296 + index;
  691. }
  692. if (index >= this.cssRules.length) {
  693. errorUtils.throwIndexError(this, 'deleteRule', this.constructor.name, index, this.cssRules.length);
  694. }
  695. this.cssRules[index].__parentRule = null;
  696. this.cssRules[index].__parentStyleSheet = null;
  697. this.cssRules.splice(index, 1);
  698. };
  699. /**
  700. * @constructor
  701. * @see https://drafts.csswg.org/css-counter-styles/#the-csscounterstylerule-interface
  702. */
  703. CSSOM.CSSCounterStyleRule = function CSSCounterStyleRule() {
  704. CSSOM.CSSRule.call(this);
  705. this.name = "";
  706. this.__props = "";
  707. };
  708. CSSOM.CSSCounterStyleRule.prototype = Object.create(CSSOM.CSSRule.prototype);
  709. CSSOM.CSSCounterStyleRule.prototype.constructor = CSSOM.CSSCounterStyleRule;
  710. Object.setPrototypeOf(CSSOM.CSSCounterStyleRule, CSSOM.CSSRule);
  711. Object.defineProperty(CSSOM.CSSCounterStyleRule.prototype, "type", {
  712. value: 11,
  713. writable: false
  714. });
  715. Object.defineProperty(CSSOM.CSSCounterStyleRule.prototype, "cssText", {
  716. get: function() {
  717. // FIXME : Implement real cssText generation based on properties
  718. return "@counter-style " + this.name + " { " + this.__props + " }";
  719. }
  720. });
  721. /**
  722. * NON-STANDARD
  723. * Rule text parser.
  724. * @param {string} cssText
  725. */
  726. Object.defineProperty(CSSOM.CSSCounterStyleRule.prototype, "parse", {
  727. value: function(cssText) {
  728. // Extract the name from "@counter-style <name> { ... }"
  729. var match = cssText.match(/@counter-style\s+([^\s{]+)\s*\{([^]*)\}/);
  730. if (match) {
  731. this.name = match[1];
  732. // Get the text inside the brackets and clean it up
  733. var propsText = match[2];
  734. this.__props = propsText.trim().replace(/\n/g, " ").replace(/(['"])(?:\\.|[^\\])*?\1|(\s{2,})/g, function (match, quote) {
  735. return quote ? match : ' ';
  736. });
  737. }
  738. }
  739. });
  740. /**
  741. * @constructor
  742. * @see https://drafts.css-houdini.org/css-properties-values-api/#the-css-property-rule-interface
  743. */
  744. CSSOM.CSSPropertyRule = function CSSPropertyRule() {
  745. CSSOM.CSSRule.call(this);
  746. this.__name = "";
  747. this.__syntax = "";
  748. this.__inherits = false;
  749. this.__initialValue = null;
  750. };
  751. CSSOM.CSSPropertyRule.prototype = Object.create(CSSOM.CSSRule.prototype);
  752. CSSOM.CSSPropertyRule.prototype.constructor = CSSOM.CSSPropertyRule;
  753. Object.setPrototypeOf(CSSOM.CSSPropertyRule, CSSOM.CSSRule);
  754. Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "type", {
  755. value: 0,
  756. writable: false
  757. });
  758. Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "cssText", {
  759. get: function() {
  760. var text = "@property " + this.name + " {";
  761. if (this.syntax !== "") {
  762. text += " syntax: \"" + this.syntax.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + "\";";
  763. }
  764. text += " inherits: " + (this.inherits ? "true" : "false") + ";";
  765. if (this.initialValue !== null) {
  766. text += " initial-value: " + this.initialValue + ";";
  767. }
  768. text += " }";
  769. return text;
  770. }
  771. });
  772. Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "name", {
  773. get: function() {
  774. return this.__name;
  775. }
  776. });
  777. Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "syntax", {
  778. get: function() {
  779. return this.__syntax;
  780. }
  781. });
  782. Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "inherits", {
  783. get: function() {
  784. return this.__inherits;
  785. }
  786. });
  787. Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "initialValue", {
  788. get: function() {
  789. return this.__initialValue;
  790. }
  791. });
  792. /**
  793. * NON-STANDARD
  794. * Rule text parser.
  795. * @param {string} cssText
  796. * @returns {boolean} True if the rule is valid and was parsed successfully
  797. */
  798. Object.defineProperty(CSSOM.CSSPropertyRule.prototype, "parse", {
  799. value: function(cssText) {
  800. // Extract the name from "@property <name> { ... }"
  801. var match = cssText.match(/@property\s+(--[^\s{]+)\s*\{([^]*)\}/);
  802. if (!match) {
  803. return false;
  804. }
  805. this.__name = match[1];
  806. var bodyText = match[2];
  807. // Parse syntax descriptor (REQUIRED)
  808. var syntaxMatch = bodyText.match(/syntax\s*:\s*(['"])([^]*?)\1\s*;/);
  809. if (!syntaxMatch) {
  810. return false; // syntax is required
  811. }
  812. this.__syntax = syntaxMatch[2];
  813. // Syntax cannot be empty
  814. if (this.__syntax === "") {
  815. return false;
  816. }
  817. // Parse inherits descriptor (REQUIRED)
  818. var inheritsMatch = bodyText.match(/inherits\s*:\s*(true|false)\s*;/);
  819. if (!inheritsMatch) {
  820. return false; // inherits is required
  821. }
  822. this.__inherits = inheritsMatch[1] === "true";
  823. // Parse initial-value descriptor (OPTIONAL, but required if syntax is not "*")
  824. var initialValueMatch = bodyText.match(/initial-value\s*:\s*([^;]+);/);
  825. if (initialValueMatch) {
  826. this.__initialValue = initialValueMatch[1].trim();
  827. } else {
  828. // If syntax is not "*", initial-value is required
  829. if (this.__syntax !== "*") {
  830. return false;
  831. }
  832. }
  833. return true; // Successfully parsed
  834. }
  835. });
  836. /**
  837. * @constructor
  838. * @see https://www.w3.org/TR/css-conditional-3/#the-cssconditionrule-interface
  839. */
  840. CSSOM.CSSConditionRule = function CSSConditionRule() {
  841. CSSOM.CSSGroupingRule.call(this);
  842. this.__conditionText = '';
  843. };
  844. CSSOM.CSSConditionRule.prototype = Object.create(CSSOM.CSSGroupingRule.prototype);
  845. CSSOM.CSSConditionRule.prototype.constructor = CSSOM.CSSConditionRule;
  846. Object.setPrototypeOf(CSSOM.CSSConditionRule, CSSOM.CSSGroupingRule);
  847. Object.defineProperty(CSSOM.CSSConditionRule.prototype, "conditionText", {
  848. get: function () {
  849. return this.__conditionText;
  850. }
  851. });
  852. /**
  853. * @constructor
  854. * @see http://dev.w3.org/csswg/cssom/#cssstylerule
  855. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleRule
  856. */
  857. CSSOM.CSSStyleRule = function CSSStyleRule() {
  858. CSSOM.CSSGroupingRule.call(this);
  859. this.__selectorText = "";
  860. this.__style = new CSSOM.CSSStyleDeclaration();
  861. this.__style.parentRule = this;
  862. };
  863. CSSOM.CSSStyleRule.prototype = Object.create(CSSOM.CSSGroupingRule.prototype);
  864. CSSOM.CSSStyleRule.prototype.constructor = CSSOM.CSSStyleRule;
  865. Object.setPrototypeOf(CSSOM.CSSStyleRule, CSSOM.CSSGroupingRule);
  866. Object.defineProperty(CSSOM.CSSStyleRule.prototype, "type", {
  867. value: 1,
  868. writable: false
  869. });
  870. Object.defineProperty(CSSOM.CSSStyleRule.prototype, "selectorText", {
  871. get: function() {
  872. return this.__selectorText;
  873. },
  874. set: function(value) {
  875. if (typeof value === "string") {
  876. // Don't trim if the value ends with a hex escape sequence followed by space
  877. // (e.g., ".\31 " where the space is part of the escape terminator)
  878. var endsWithHexEscapeRegExp = regexPatterns.endsWithHexEscapeRegExp;
  879. var endsWithEscape = endsWithHexEscapeRegExp.test(value);
  880. var trimmedValue = endsWithEscape ? value.replace(/\s+$/, ' ').trimStart() : value.trim();
  881. if (trimmedValue === '') {
  882. return;
  883. }
  884. // TODO: Setting invalid selectorText should be ignored
  885. // There are some validations already on lib/parse.js
  886. // but the same validations should be applied here.
  887. // Check if we can move these validation logic to a shared function.
  888. this.__selectorText = trimmedValue;
  889. }
  890. },
  891. configurable: true
  892. });
  893. Object.defineProperty(CSSOM.CSSStyleRule.prototype, "style", {
  894. get: function() {
  895. return this.__style;
  896. },
  897. set: function(value) {
  898. if (typeof value === "string") {
  899. this.__style.cssText = value;
  900. } else {
  901. this.__style = value;
  902. }
  903. },
  904. configurable: true
  905. });
  906. Object.defineProperty(CSSOM.CSSStyleRule.prototype, "cssText", {
  907. get: function() {
  908. var text;
  909. if (this.selectorText) {
  910. var values = "";
  911. if (this.cssRules.length) {
  912. var valuesArr = [" {"];
  913. this.style.cssText && valuesArr.push(this.style.cssText);
  914. valuesArr.push(this.cssRules.reduce(function(acc, rule){
  915. if (rule.cssText !== "") {
  916. acc.push(rule.cssText);
  917. }
  918. return acc;
  919. }, []).join("\n "));
  920. values = valuesArr.join("\n ") + "\n}";
  921. } else {
  922. values = " {" + (this.style.cssText ? " " + this.style.cssText : "") + " }";
  923. }
  924. text = this.selectorText + values;
  925. } else {
  926. text = "";
  927. }
  928. return text;
  929. }
  930. });
  931. /**
  932. * @constructor
  933. * @see http://dev.w3.org/csswg/cssom/#the-medialist-interface
  934. */
  935. CSSOM.MediaList = function MediaList(){
  936. this.length = 0;
  937. };
  938. CSSOM.MediaList.prototype = {
  939. constructor: CSSOM.MediaList,
  940. /**
  941. * @return {string}
  942. */
  943. get mediaText() {
  944. return Array.prototype.join.call(this, ", ");
  945. },
  946. /**
  947. * @param {string} value
  948. */
  949. set mediaText(value) {
  950. if (typeof value === "string") {
  951. var values = value.split(",").filter(function(text){
  952. return !!text;
  953. });
  954. var length = this.length = values.length;
  955. for (var i=0; i<length; i++) {
  956. this[i] = values[i].trim();
  957. }
  958. } else if (value === null) {
  959. var length = this.length;
  960. for (var i = 0; i < length; i++) {
  961. delete this[i];
  962. }
  963. this.length = 0;
  964. }
  965. },
  966. /**
  967. * @param {string} medium
  968. */
  969. appendMedium: function(medium) {
  970. if (Array.prototype.indexOf.call(this, medium) === -1) {
  971. this[this.length] = medium;
  972. this.length++;
  973. }
  974. },
  975. /**
  976. * @param {string} medium
  977. */
  978. deleteMedium: function(medium) {
  979. var index = Array.prototype.indexOf.call(this, medium);
  980. if (index !== -1) {
  981. Array.prototype.splice.call(this, index, 1);
  982. }
  983. },
  984. item: function(index) {
  985. return this[index] || null;
  986. },
  987. toString: function() {
  988. return this.mediaText;
  989. }
  990. };
  991. /**
  992. * @constructor
  993. * @see http://dev.w3.org/csswg/cssom/#cssmediarule
  994. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSMediaRule
  995. */
  996. CSSOM.CSSMediaRule = function CSSMediaRule() {
  997. CSSOM.CSSConditionRule.call(this);
  998. this.__media = new CSSOM.MediaList();
  999. };
  1000. CSSOM.CSSMediaRule.prototype = Object.create(CSSOM.CSSConditionRule.prototype);
  1001. CSSOM.CSSMediaRule.prototype.constructor = CSSOM.CSSMediaRule;
  1002. Object.setPrototypeOf(CSSOM.CSSMediaRule, CSSOM.CSSConditionRule);
  1003. Object.defineProperty(CSSOM.CSSMediaRule.prototype, "type", {
  1004. value: 4,
  1005. writable: false
  1006. });
  1007. // https://opensource.apple.com/source/WebCore/WebCore-7611.1.21.161.3/css/CSSMediaRule.cpp
  1008. Object.defineProperties(CSSOM.CSSMediaRule.prototype, {
  1009. "media": {
  1010. get: function() {
  1011. return this.__media;
  1012. },
  1013. set: function(value) {
  1014. if (typeof value === "string") {
  1015. this.__media.mediaText = value;
  1016. } else {
  1017. this.__media = value;
  1018. }
  1019. },
  1020. configurable: true,
  1021. enumerable: true
  1022. },
  1023. "conditionText": {
  1024. get: function() {
  1025. return this.media.mediaText;
  1026. }
  1027. },
  1028. "cssText": {
  1029. get: function() {
  1030. var values = "";
  1031. var valuesArr = [" {"];
  1032. if (this.cssRules.length) {
  1033. valuesArr.push(this.cssRules.reduce(function(acc, rule){
  1034. if (rule.cssText !== "") {
  1035. acc.push(rule.cssText);
  1036. }
  1037. return acc;
  1038. }, []).join("\n "));
  1039. }
  1040. values = valuesArr.join("\n ") + "\n}";
  1041. return "@media " + this.media.mediaText + values;
  1042. }
  1043. }
  1044. });
  1045. /**
  1046. * @constructor
  1047. * @see https://drafts.csswg.org/css-contain-3/
  1048. * @see https://www.w3.org/TR/css-contain-3/
  1049. */
  1050. CSSOM.CSSContainerRule = function CSSContainerRule() {
  1051. CSSOM.CSSConditionRule.call(this);
  1052. };
  1053. CSSOM.CSSContainerRule.prototype = Object.create(CSSOM.CSSConditionRule.prototype);
  1054. CSSOM.CSSContainerRule.prototype.constructor = CSSOM.CSSContainerRule;
  1055. Object.setPrototypeOf(CSSOM.CSSContainerRule, CSSOM.CSSConditionRule);
  1056. Object.defineProperty(CSSOM.CSSContainerRule.prototype, "type", {
  1057. value: 17,
  1058. writable: false
  1059. });
  1060. Object.defineProperties(CSSOM.CSSContainerRule.prototype, {
  1061. "cssText": {
  1062. get: function() {
  1063. var values = "";
  1064. var valuesArr = [" {"];
  1065. if (this.cssRules.length) {
  1066. valuesArr.push(this.cssRules.reduce(function(acc, rule){
  1067. if (rule.cssText !== "") {
  1068. acc.push(rule.cssText);
  1069. }
  1070. return acc;
  1071. }, []).join("\n "));
  1072. }
  1073. values = valuesArr.join("\n ") + "\n}";
  1074. return "@container " + this.conditionText + values;
  1075. }
  1076. },
  1077. "containerName": {
  1078. get: function() {
  1079. var parts = this.conditionText.trim().split(/\s+/);
  1080. if (parts.length > 1 && parts[0] !== '(' && !parts[0].startsWith('(')) {
  1081. return parts[0];
  1082. }
  1083. return "";
  1084. }
  1085. },
  1086. "containerQuery": {
  1087. get: function() {
  1088. var parts = this.conditionText.trim().split(/\s+/);
  1089. if (parts.length > 1 && parts[0] !== '(' && !parts[0].startsWith('(')) {
  1090. return parts.slice(1).join(' ');
  1091. }
  1092. return this.conditionText;
  1093. }
  1094. },
  1095. });
  1096. /**
  1097. * @constructor
  1098. * @see https://drafts.csswg.org/css-conditional-3/#the-csssupportsrule-interface
  1099. */
  1100. CSSOM.CSSSupportsRule = function CSSSupportsRule() {
  1101. CSSOM.CSSConditionRule.call(this);
  1102. };
  1103. CSSOM.CSSSupportsRule.prototype = Object.create(CSSOM.CSSConditionRule.prototype);
  1104. CSSOM.CSSSupportsRule.prototype.constructor = CSSOM.CSSSupportsRule;
  1105. Object.setPrototypeOf(CSSOM.CSSSupportsRule, CSSOM.CSSConditionRule);
  1106. Object.defineProperty(CSSOM.CSSSupportsRule.prototype, "type", {
  1107. value: 12,
  1108. writable: false
  1109. });
  1110. Object.defineProperty(CSSOM.CSSSupportsRule.prototype, "cssText", {
  1111. get: function() {
  1112. var values = "";
  1113. var valuesArr = [" {"];
  1114. if (this.cssRules.length) {
  1115. valuesArr.push(this.cssRules.reduce(function(acc, rule){
  1116. if (rule.cssText !== "") {
  1117. acc.push(rule.cssText);
  1118. }
  1119. return acc;
  1120. }, []).join("\n "));
  1121. }
  1122. values = valuesArr.join("\n ") + "\n}";
  1123. return "@supports " + this.conditionText + values;
  1124. }
  1125. });
  1126. /**
  1127. * @constructor
  1128. * @see http://dev.w3.org/csswg/cssom/#cssimportrule
  1129. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSImportRule
  1130. */
  1131. CSSOM.CSSImportRule = function CSSImportRule() {
  1132. CSSOM.CSSRule.call(this);
  1133. this.__href = "";
  1134. this.__media = new CSSOM.MediaList();
  1135. this.__layerName = null;
  1136. this.__supportsText = null;
  1137. this.__styleSheet = new CSSOM.CSSStyleSheet();
  1138. };
  1139. CSSOM.CSSImportRule.prototype = Object.create(CSSOM.CSSRule.prototype);
  1140. CSSOM.CSSImportRule.prototype.constructor = CSSOM.CSSImportRule;
  1141. Object.setPrototypeOf(CSSOM.CSSImportRule, CSSOM.CSSRule);
  1142. Object.defineProperty(CSSOM.CSSImportRule.prototype, "type", {
  1143. value: 3,
  1144. writable: false
  1145. });
  1146. Object.defineProperty(CSSOM.CSSImportRule.prototype, "cssText", {
  1147. get: function() {
  1148. var mediaText = this.media.mediaText;
  1149. return "@import url(\"" + this.href.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + "\")" + (this.layerName !== null ? " layer" + (this.layerName && "(" + this.layerName + ")") : "" ) + (this.supportsText ? " supports(" + this.supportsText + ")" : "" ) + (mediaText ? " " + mediaText : "") + ";";
  1150. }
  1151. });
  1152. Object.defineProperty(CSSOM.CSSImportRule.prototype, "href", {
  1153. get: function() {
  1154. return this.__href;
  1155. }
  1156. });
  1157. Object.defineProperty(CSSOM.CSSImportRule.prototype, "media", {
  1158. get: function() {
  1159. return this.__media;
  1160. },
  1161. set: function(value) {
  1162. if (typeof value === "string") {
  1163. this.__media.mediaText = value;
  1164. } else {
  1165. this.__media = value;
  1166. }
  1167. }
  1168. });
  1169. Object.defineProperty(CSSOM.CSSImportRule.prototype, "layerName", {
  1170. get: function() {
  1171. return this.__layerName;
  1172. }
  1173. });
  1174. Object.defineProperty(CSSOM.CSSImportRule.prototype, "supportsText", {
  1175. get: function() {
  1176. return this.__supportsText;
  1177. }
  1178. });
  1179. Object.defineProperty(CSSOM.CSSImportRule.prototype, "styleSheet", {
  1180. get: function() {
  1181. return this.__styleSheet;
  1182. }
  1183. });
  1184. /**
  1185. * NON-STANDARD
  1186. * Rule text parser.
  1187. * @param {string} cssText
  1188. */
  1189. Object.defineProperty(CSSOM.CSSImportRule.prototype, "parse", {
  1190. value: function(cssText) {
  1191. var i = 0;
  1192. /**
  1193. * @import url(partial.css) screen, handheld;
  1194. * || |
  1195. * after-import media
  1196. * |
  1197. * url
  1198. */
  1199. var state = '';
  1200. var buffer = '';
  1201. var index;
  1202. var layerRegExp = regexPatterns.layerRegExp;
  1203. var layerRuleNameRegExp = regexPatterns.layerRuleNameRegExp;
  1204. var doubleOrMoreSpacesRegExp = regexPatterns.doubleOrMoreSpacesRegExp;
  1205. /**
  1206. * Extracts the content inside supports() handling nested parentheses.
  1207. * @param {string} text - The text to parse
  1208. * @returns {object|null} - {content: string, endIndex: number} or null if not found
  1209. */
  1210. function extractSupportsContent(text) {
  1211. var supportsIndex = text.indexOf('supports(');
  1212. if (supportsIndex !== 0) {
  1213. return null;
  1214. }
  1215. var depth = 0;
  1216. var start = supportsIndex + 'supports('.length;
  1217. var i = start;
  1218. for (; i < text.length; i++) {
  1219. if (text[i] === '(') {
  1220. depth++;
  1221. } else if (text[i] === ')') {
  1222. if (depth === 0) {
  1223. // Found the closing parenthesis for supports()
  1224. return {
  1225. content: text.slice(start, i),
  1226. endIndex: i
  1227. };
  1228. }
  1229. depth--;
  1230. }
  1231. }
  1232. return null; // Unbalanced parentheses
  1233. }
  1234. for (var character; (character = cssText.charAt(i)); i++) {
  1235. switch (character) {
  1236. case ' ':
  1237. case '\t':
  1238. case '\r':
  1239. case '\n':
  1240. case '\f':
  1241. if (state === 'after-import') {
  1242. state = 'url';
  1243. } else {
  1244. buffer += character;
  1245. }
  1246. break;
  1247. case '@':
  1248. if (!state && cssText.indexOf('@import', i) === i) {
  1249. state = 'after-import';
  1250. i += 'import'.length;
  1251. buffer = '';
  1252. }
  1253. break;
  1254. case 'u':
  1255. if (state === 'media') {
  1256. buffer += character;
  1257. }
  1258. if (state === 'url' && cssText.indexOf('url(', i) === i) {
  1259. index = cssText.indexOf(')', i + 1);
  1260. if (index === -1) {
  1261. throw i + ': ")" not found';
  1262. }
  1263. i += 'url('.length;
  1264. var url = cssText.slice(i, index);
  1265. if (url[0] === url[url.length - 1]) {
  1266. if (url[0] === '"' || url[0] === "'") {
  1267. url = url.slice(1, -1);
  1268. }
  1269. }
  1270. this.__href = url;
  1271. i = index;
  1272. state = 'media';
  1273. }
  1274. break;
  1275. case '"':
  1276. if (state === 'after-import' || state === 'url') {
  1277. index = cssText.indexOf('"', i + 1);
  1278. if (!index) {
  1279. throw i + ": '\"' not found";
  1280. }
  1281. this.__href = cssText.slice(i + 1, index);
  1282. i = index;
  1283. state = 'media';
  1284. }
  1285. break;
  1286. case "'":
  1287. if (state === 'after-import' || state === 'url') {
  1288. index = cssText.indexOf("'", i + 1);
  1289. if (!index) {
  1290. throw i + ': "\'" not found';
  1291. }
  1292. this.__href = cssText.slice(i + 1, index);
  1293. i = index;
  1294. state = 'media';
  1295. }
  1296. break;
  1297. case ';':
  1298. if (state === 'media') {
  1299. if (buffer) {
  1300. var bufferTrimmed = buffer.trim();
  1301. if (bufferTrimmed.indexOf('layer') === 0) {
  1302. var layerMatch = bufferTrimmed.match(layerRegExp);
  1303. if (layerMatch) {
  1304. var layerName = layerMatch[1].trim();
  1305. if (layerName.match(layerRuleNameRegExp) !== null) {
  1306. this.__layerName = layerMatch[1].trim();
  1307. bufferTrimmed = bufferTrimmed.replace(layerRegExp, '')
  1308. .replace(doubleOrMoreSpacesRegExp, ' ') // Replace double or more spaces with single space
  1309. .trim();
  1310. } else {
  1311. // REVIEW: In the browser, an empty layer() is not processed as a unamed layer
  1312. // and treats the rest of the string as mediaText, ignoring the parse of supports()
  1313. if (bufferTrimmed) {
  1314. this.media.mediaText = bufferTrimmed;
  1315. return;
  1316. }
  1317. }
  1318. } else {
  1319. this.__layerName = "";
  1320. bufferTrimmed = bufferTrimmed.substring('layer'.length).trim()
  1321. }
  1322. }
  1323. var supportsResult = extractSupportsContent(bufferTrimmed);
  1324. if (supportsResult) {
  1325. // REVIEW: In the browser, an empty supports() invalidates and ignores the entire @import rule
  1326. this.__supportsText = supportsResult.content.trim();
  1327. // Remove the entire supports(...) from the buffer
  1328. bufferTrimmed = bufferTrimmed.slice(0, 0) + bufferTrimmed.slice(supportsResult.endIndex + 1);
  1329. bufferTrimmed = bufferTrimmed.replace(doubleOrMoreSpacesRegExp, ' ').trim();
  1330. }
  1331. // REVIEW: In the browser, any invalid media is replaced with 'not all'
  1332. if (bufferTrimmed) {
  1333. this.media.mediaText = bufferTrimmed;
  1334. }
  1335. }
  1336. }
  1337. break;
  1338. default:
  1339. if (state === 'media') {
  1340. buffer += character;
  1341. }
  1342. break;
  1343. }
  1344. }
  1345. }
  1346. });
  1347. /**
  1348. * @constructor
  1349. * @see https://drafts.csswg.org/cssom/#the-cssnamespacerule-interface
  1350. */
  1351. CSSOM.CSSNamespaceRule = function CSSNamespaceRule() {
  1352. CSSOM.CSSRule.call(this);
  1353. this.__prefix = "";
  1354. this.__namespaceURI = "";
  1355. };
  1356. CSSOM.CSSNamespaceRule.prototype = Object.create(CSSOM.CSSRule.prototype);
  1357. CSSOM.CSSNamespaceRule.prototype.constructor = CSSOM.CSSNamespaceRule;
  1358. Object.setPrototypeOf(CSSOM.CSSNamespaceRule, CSSOM.CSSRule);
  1359. Object.defineProperty(CSSOM.CSSNamespaceRule.prototype, "type", {
  1360. value: 10,
  1361. writable: false
  1362. });
  1363. Object.defineProperty(CSSOM.CSSNamespaceRule.prototype, "cssText", {
  1364. get: function() {
  1365. return "@namespace" + (this.prefix && " " + this.prefix) + " url(\"" + this.namespaceURI + "\");";
  1366. }
  1367. });
  1368. Object.defineProperty(CSSOM.CSSNamespaceRule.prototype, "prefix", {
  1369. get: function() {
  1370. return this.__prefix;
  1371. }
  1372. });
  1373. Object.defineProperty(CSSOM.CSSNamespaceRule.prototype, "namespaceURI", {
  1374. get: function() {
  1375. return this.__namespaceURI;
  1376. }
  1377. });
  1378. /**
  1379. * NON-STANDARD
  1380. * Rule text parser.
  1381. * @param {string} cssText
  1382. */
  1383. Object.defineProperty(CSSOM.CSSNamespaceRule.prototype, "parse", {
  1384. value: function(cssText) {
  1385. var newPrefix = "";
  1386. var newNamespaceURI = "";
  1387. // Remove @namespace and trim
  1388. var text = cssText.trim();
  1389. if (text.indexOf('@namespace') === 0) {
  1390. text = text.slice('@namespace'.length).trim();
  1391. }
  1392. // Remove trailing semicolon if present
  1393. if (text.charAt(text.length - 1) === ';') {
  1394. text = text.slice(0, -1).trim();
  1395. }
  1396. // Regex to match valid namespace syntax:
  1397. // 1. [optional prefix] url("...") or [optional prefix] url('...') or [optional prefix] url() or [optional prefix] url(unquoted)
  1398. // 2. [optional prefix] "..." or [optional prefix] '...'
  1399. // The prefix must be a valid CSS identifier (letters, digits, hyphens, underscores, starting with letter or underscore)
  1400. var re = /^(?:([a-zA-Z_][a-zA-Z0-9_-]*)\s+)?(?:url\(\s*(?:(['"])(.*?)\2\s*|([^)]*?))\s*\)|(['"])(.*?)\5)$/;
  1401. var match = text.match(re);
  1402. if (match) {
  1403. // If prefix is present
  1404. if (match[1]) {
  1405. newPrefix = match[1];
  1406. }
  1407. // If url(...) form with quotes
  1408. if (typeof match[3] !== "undefined") {
  1409. newNamespaceURI = match[3];
  1410. }
  1411. // If url(...) form without quotes
  1412. else if (typeof match[4] !== "undefined") {
  1413. newNamespaceURI = match[4].trim();
  1414. }
  1415. // If quoted string form
  1416. else if (typeof match[6] !== "undefined") {
  1417. newNamespaceURI = match[6];
  1418. }
  1419. this.__prefix = newPrefix;
  1420. this.__namespaceURI = newNamespaceURI;
  1421. } else {
  1422. throw new DOMException("Invalid @namespace rule", "InvalidStateError");
  1423. }
  1424. }
  1425. });
  1426. /**
  1427. * @constructor
  1428. * @see http://dev.w3.org/csswg/cssom/#css-font-face-rule
  1429. */
  1430. CSSOM.CSSFontFaceRule = function CSSFontFaceRule() {
  1431. CSSOM.CSSRule.call(this);
  1432. this.__style = new CSSOM.CSSStyleDeclaration();
  1433. this.__style.parentRule = this;
  1434. };
  1435. CSSOM.CSSFontFaceRule.prototype = Object.create(CSSOM.CSSRule.prototype);
  1436. CSSOM.CSSFontFaceRule.prototype.constructor = CSSOM.CSSFontFaceRule;
  1437. Object.setPrototypeOf(CSSOM.CSSFontFaceRule, CSSOM.CSSRule);
  1438. Object.defineProperty(CSSOM.CSSFontFaceRule.prototype, "type", {
  1439. value: 5,
  1440. writable: false
  1441. });
  1442. //FIXME
  1443. //CSSOM.CSSFontFaceRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
  1444. //CSSOM.CSSFontFaceRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
  1445. Object.defineProperty(CSSOM.CSSFontFaceRule.prototype, "style", {
  1446. get: function() {
  1447. return this.__style;
  1448. },
  1449. set: function(value) {
  1450. if (typeof value === "string") {
  1451. this.__style.cssText = value;
  1452. } else {
  1453. this.__style = value;
  1454. }
  1455. }
  1456. });
  1457. // http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSFontFaceRule.cpp
  1458. Object.defineProperty(CSSOM.CSSFontFaceRule.prototype, "cssText", {
  1459. get: function() {
  1460. return "@font-face {" + (this.style.cssText ? " " + this.style.cssText : "") + " }";
  1461. }
  1462. });
  1463. /**
  1464. * @constructor
  1465. * @see http://www.w3.org/TR/shadow-dom/#host-at-rule
  1466. * @see http://html5index.org/Shadow%20DOM%20-%20CSSHostRule.html
  1467. * @deprecated This rule was part of early Shadow DOM drafts but was removed in favor of the more flexible :host and :host-context() pseudo-classes in modern CSS for Web Components.
  1468. */
  1469. CSSOM.CSSHostRule = function CSSHostRule() {
  1470. CSSOM.CSSRule.call(this);
  1471. this.cssRules = new CSSOM.CSSRuleList();
  1472. };
  1473. CSSOM.CSSHostRule.prototype = Object.create(CSSOM.CSSRule.prototype);
  1474. CSSOM.CSSHostRule.prototype.constructor = CSSOM.CSSHostRule;
  1475. Object.setPrototypeOf(CSSOM.CSSHostRule, CSSOM.CSSRule);
  1476. Object.defineProperty(CSSOM.CSSHostRule.prototype, "type", {
  1477. value: 1001,
  1478. writable: false
  1479. });
  1480. //FIXME
  1481. //CSSOM.CSSHostRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
  1482. //CSSOM.CSSHostRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
  1483. Object.defineProperty(CSSOM.CSSHostRule.prototype, "cssText", {
  1484. get: function() {
  1485. var values = "";
  1486. var valuesArr = [" {"];
  1487. if (this.cssRules.length) {
  1488. valuesArr.push(this.cssRules.reduce(function(acc, rule){
  1489. if (rule.cssText !== "") {
  1490. acc.push(rule.cssText);
  1491. }
  1492. return acc;
  1493. }, []).join("\n "));
  1494. }
  1495. values = valuesArr.join("\n ") + "\n}";
  1496. return "@host" + values;
  1497. }
  1498. });
  1499. /**
  1500. * @constructor
  1501. * @see http://www.w3.org/TR/shadow-dom/#host-at-rule
  1502. */
  1503. CSSOM.CSSStartingStyleRule = function CSSStartingStyleRule() {
  1504. CSSOM.CSSGroupingRule.call(this);
  1505. };
  1506. CSSOM.CSSStartingStyleRule.prototype = Object.create(CSSOM.CSSGroupingRule.prototype);
  1507. CSSOM.CSSStartingStyleRule.prototype.constructor = CSSOM.CSSStartingStyleRule;
  1508. Object.setPrototypeOf(CSSOM.CSSStartingStyleRule, CSSOM.CSSGroupingRule);
  1509. Object.defineProperty(CSSOM.CSSStartingStyleRule.prototype, "type", {
  1510. value: 1002,
  1511. writable: false
  1512. });
  1513. //FIXME
  1514. //CSSOM.CSSStartingStyleRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
  1515. //CSSOM.CSSStartingStyleRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
  1516. Object.defineProperty(CSSOM.CSSStartingStyleRule.prototype, "cssText", {
  1517. get: function() {
  1518. var values = "";
  1519. var valuesArr = [" {"];
  1520. if (this.cssRules.length) {
  1521. valuesArr.push(this.cssRules.reduce(function(acc, rule){
  1522. if (rule.cssText !== "") {
  1523. acc.push(rule.cssText);
  1524. }
  1525. return acc;
  1526. }, []).join("\n "));
  1527. }
  1528. values = valuesArr.join("\n ") + "\n}";
  1529. return "@starting-style" + values;
  1530. }
  1531. });
  1532. /**
  1533. * @see http://dev.w3.org/csswg/cssom/#the-stylesheet-interface
  1534. */
  1535. CSSOM.StyleSheet = function StyleSheet() {
  1536. this.__href = null;
  1537. this.__ownerNode = null;
  1538. this.__title = null;
  1539. this.__media = new CSSOM.MediaList();
  1540. this.__parentStyleSheet = null;
  1541. this.disabled = false;
  1542. };
  1543. Object.defineProperties(CSSOM.StyleSheet.prototype, {
  1544. type: {
  1545. get: function() {
  1546. return "text/css";
  1547. }
  1548. },
  1549. href: {
  1550. get: function() {
  1551. return this.__href;
  1552. }
  1553. },
  1554. ownerNode: {
  1555. get: function() {
  1556. return this.__ownerNode;
  1557. }
  1558. },
  1559. title: {
  1560. get: function() {
  1561. return this.__title;
  1562. }
  1563. },
  1564. media: {
  1565. get: function() {
  1566. return this.__media;
  1567. },
  1568. set: function(value) {
  1569. if (typeof value === "string") {
  1570. this.__media.mediaText = value;
  1571. } else {
  1572. this.__media = value;
  1573. }
  1574. }
  1575. },
  1576. parentStyleSheet: {
  1577. get: function() {
  1578. return this.__parentStyleSheet;
  1579. }
  1580. }
  1581. });
  1582. /**
  1583. * @constructor
  1584. * @param {CSSStyleSheetInit} [opts] - CSSStyleSheetInit options.
  1585. * @param {string} [opts.baseURL] - The base URL of the stylesheet.
  1586. * @param {boolean} [opts.disabled] - The disabled attribute of the stylesheet.
  1587. * @param {MediaList | string} [opts.media] - The media attribute of the stylesheet.
  1588. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet
  1589. */
  1590. CSSOM.CSSStyleSheet = function CSSStyleSheet(opts) {
  1591. CSSOM.StyleSheet.call(this);
  1592. this.__constructed = true;
  1593. this.__cssRules = new CSSOM.CSSRuleList();
  1594. this.__ownerRule = null;
  1595. if (opts && typeof opts === "object") {
  1596. if (opts.baseURL && typeof opts.baseURL === "string") {
  1597. this.__baseURL = opts.baseURL;
  1598. }
  1599. if (opts.media && typeof opts.media === "string") {
  1600. this.media.mediaText = opts.media;
  1601. }
  1602. if (typeof opts.disabled === "boolean") {
  1603. this.disabled = opts.disabled;
  1604. }
  1605. }
  1606. };
  1607. CSSOM.CSSStyleSheet.prototype = Object.create(CSSOM.StyleSheet.prototype);
  1608. CSSOM.CSSStyleSheet.prototype.constructor = CSSOM.CSSStyleSheet;
  1609. Object.setPrototypeOf(CSSOM.CSSStyleSheet, CSSOM.StyleSheet);
  1610. Object.defineProperty(CSSOM.CSSStyleSheet.prototype, "cssRules", {
  1611. get: function() {
  1612. return this.__cssRules;
  1613. }
  1614. });
  1615. Object.defineProperty(CSSOM.CSSStyleSheet.prototype, "rules", {
  1616. get: function() {
  1617. return this.__cssRules;
  1618. }
  1619. });
  1620. Object.defineProperty(CSSOM.CSSStyleSheet.prototype, "ownerRule", {
  1621. get: function() {
  1622. return this.__ownerRule;
  1623. }
  1624. });
  1625. /**
  1626. * Used to insert a new rule into the style sheet. The new rule now becomes part of the cascade.
  1627. *
  1628. * sheet = new Sheet("body {margin: 0}")
  1629. * sheet.toString()
  1630. * -> "body{margin:0;}"
  1631. * sheet.insertRule("img {border: none}", 0)
  1632. * -> 0
  1633. * sheet.toString()
  1634. * -> "img{border:none;}body{margin:0;}"
  1635. *
  1636. * @param {string} rule
  1637. * @param {number} [index=0]
  1638. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet-insertRule
  1639. * @return {number} The index within the style sheet's rule collection of the newly inserted rule.
  1640. */
  1641. CSSOM.CSSStyleSheet.prototype.insertRule = function(rule, index) {
  1642. if (rule === undefined && index === undefined) {
  1643. errorUtils.throwMissingArguments(this, 'insertRule', this.constructor.name);
  1644. }
  1645. if (index === void 0) {
  1646. index = 0;
  1647. }
  1648. index = Number(index);
  1649. if (index < 0) {
  1650. index = 4294967296 + index;
  1651. }
  1652. if (index > this.cssRules.length) {
  1653. errorUtils.throwIndexError(this, 'insertRule', this.constructor.name, index, this.cssRules.length);
  1654. }
  1655. var ruleToParse = String(rule);
  1656. var parseErrors = [];
  1657. var parsedSheet = CSSOM.parse(ruleToParse, undefined, function(err) {
  1658. parseErrors.push(err);
  1659. } );
  1660. if (parsedSheet.cssRules.length !== 1) {
  1661. errorUtils.throwParseError(this, 'insertRule', this.constructor.name, ruleToParse, 'SyntaxError');
  1662. }
  1663. var cssRule = parsedSheet.cssRules[0];
  1664. // Helper function to find the last index of a specific rule constructor
  1665. function findLastIndexOfConstructor(rules, constructorName) {
  1666. for (var i = rules.length - 1; i >= 0; i--) {
  1667. if (rules[i].constructor.name === constructorName) {
  1668. return i;
  1669. }
  1670. }
  1671. return -1;
  1672. }
  1673. // Helper function to find the first index of a rule that's NOT of specified constructors
  1674. function findFirstNonConstructorIndex(rules, constructorNames) {
  1675. for (var i = 0; i < rules.length; i++) {
  1676. if (constructorNames.indexOf(rules[i].constructor.name) === -1) {
  1677. return i;
  1678. }
  1679. }
  1680. return rules.length;
  1681. }
  1682. // Validate rule ordering based on CSS specification
  1683. if (cssRule.constructor.name === 'CSSImportRule') {
  1684. if (this.__constructed === true) {
  1685. errorUtils.throwError(this, 'DOMException',
  1686. "Failed to execute 'insertRule' on '" + this.constructor.name + "': Can't insert @import rules into a constructed stylesheet.",
  1687. 'SyntaxError');
  1688. }
  1689. // @import rules cannot be inserted after @layer rules that already exist
  1690. // They can only be inserted at the beginning or after other @import rules
  1691. var firstLayerIndex = findFirstNonConstructorIndex(this.cssRules, ['CSSImportRule']);
  1692. if (firstLayerIndex < this.cssRules.length && this.cssRules[firstLayerIndex].constructor.name === 'CSSLayerStatementRule' && index > firstLayerIndex) {
  1693. errorUtils.throwError(this, 'DOMException',
  1694. "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
  1695. 'HierarchyRequestError');
  1696. }
  1697. // Also cannot insert after @namespace or other rules
  1698. var firstNonImportIndex = findFirstNonConstructorIndex(this.cssRules, ['CSSImportRule']);
  1699. if (index > firstNonImportIndex && firstNonImportIndex < this.cssRules.length &&
  1700. this.cssRules[firstNonImportIndex].constructor.name !== 'CSSLayerStatementRule') {
  1701. errorUtils.throwError(this, 'DOMException',
  1702. "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
  1703. 'HierarchyRequestError');
  1704. }
  1705. } else if (cssRule.constructor.name === 'CSSNamespaceRule') {
  1706. // @namespace rules can come after @layer and @import, but before any other rules
  1707. // They cannot come before @import rules
  1708. var firstImportIndex = -1;
  1709. for (var i = 0; i < this.cssRules.length; i++) {
  1710. if (this.cssRules[i].constructor.name === 'CSSImportRule') {
  1711. firstImportIndex = i;
  1712. break;
  1713. }
  1714. }
  1715. var firstNonImportNamespaceIndex = findFirstNonConstructorIndex(this.cssRules, [
  1716. 'CSSLayerStatementRule',
  1717. 'CSSImportRule',
  1718. 'CSSNamespaceRule'
  1719. ]);
  1720. // Cannot insert before @import rules
  1721. if (firstImportIndex !== -1 && index <= firstImportIndex) {
  1722. errorUtils.throwError(this, 'DOMException',
  1723. "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
  1724. 'HierarchyRequestError');
  1725. }
  1726. // Cannot insert if there are already non-special rules
  1727. if (firstNonImportNamespaceIndex < this.cssRules.length) {
  1728. errorUtils.throwError(this, 'DOMException',
  1729. "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
  1730. 'InvalidStateError');
  1731. }
  1732. // Cannot insert after other types of rules
  1733. if (index > firstNonImportNamespaceIndex) {
  1734. errorUtils.throwError(this, 'DOMException',
  1735. "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
  1736. 'HierarchyRequestError');
  1737. }
  1738. } else if (cssRule.constructor.name === 'CSSLayerStatementRule') {
  1739. // @layer statement rules can be inserted anywhere before @import and @namespace
  1740. // No additional restrictions beyond what's already handled
  1741. } else {
  1742. // Any other rule cannot be inserted before @import and @namespace
  1743. var firstNonSpecialRuleIndex = findFirstNonConstructorIndex(this.cssRules, [
  1744. 'CSSLayerStatementRule',
  1745. 'CSSImportRule',
  1746. 'CSSNamespaceRule'
  1747. ]);
  1748. if (index < firstNonSpecialRuleIndex) {
  1749. errorUtils.throwError(this, 'DOMException',
  1750. "Failed to execute 'insertRule' on '" + this.constructor.name + "': Failed to insert the rule.",
  1751. 'HierarchyRequestError');
  1752. }
  1753. if (parseErrors.filter(function(error) { return !error.isNested; }).length !== 0) {
  1754. errorUtils.throwParseError(this, 'insertRule', this.constructor.name, ruleToParse, 'SyntaxError');
  1755. }
  1756. }
  1757. cssRule.__parentStyleSheet = this;
  1758. this.cssRules.splice(index, 0, cssRule);
  1759. return index;
  1760. };
  1761. CSSOM.CSSStyleSheet.prototype.addRule = function(selector, styleBlock, index) {
  1762. if (index === void 0) {
  1763. index = this.cssRules.length;
  1764. }
  1765. this.insertRule(selector + "{" + styleBlock + "}", index);
  1766. return -1;
  1767. };
  1768. /**
  1769. * Used to delete a rule from the style sheet.
  1770. *
  1771. * sheet = new Sheet("img{border:none} body{margin:0}")
  1772. * sheet.toString()
  1773. * -> "img{border:none;}body{margin:0;}"
  1774. * sheet.deleteRule(0)
  1775. * sheet.toString()
  1776. * -> "body{margin:0;}"
  1777. *
  1778. * @param {number} index within the style sheet's rule list of the rule to remove.
  1779. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleSheet-deleteRule
  1780. */
  1781. CSSOM.CSSStyleSheet.prototype.deleteRule = function(index) {
  1782. if (index === undefined) {
  1783. errorUtils.throwMissingArguments(this, 'deleteRule', this.constructor.name);
  1784. }
  1785. index = Number(index);
  1786. if (index < 0) {
  1787. index = 4294967296 + index;
  1788. }
  1789. if (index >= this.cssRules.length) {
  1790. errorUtils.throwIndexError(this, 'deleteRule', this.constructor.name, index, this.cssRules.length);
  1791. }
  1792. if (this.cssRules[index]) {
  1793. if (this.cssRules[index].constructor.name == "CSSNamespaceRule") {
  1794. var shouldContinue = this.cssRules.every(function (rule) {
  1795. return ['CSSImportRule','CSSLayerStatementRule','CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1
  1796. });
  1797. if (!shouldContinue) {
  1798. errorUtils.throwError(this, 'DOMException', "Failed to execute 'deleteRule' on '" + this.constructor.name + "': Failed to delete rule.", "InvalidStateError");
  1799. }
  1800. }
  1801. if (this.cssRules[index].constructor.name == "CSSImportRule") {
  1802. this.cssRules[index].styleSheet.__parentStyleSheet = null;
  1803. }
  1804. this.cssRules[index].__parentStyleSheet = null;
  1805. }
  1806. this.cssRules.splice(index, 1);
  1807. };
  1808. CSSOM.CSSStyleSheet.prototype.removeRule = function(index) {
  1809. if (index === void 0) {
  1810. index = 0;
  1811. }
  1812. this.deleteRule(index);
  1813. };
  1814. /**
  1815. * Replaces the rules of a {@link CSSStyleSheet}
  1816. *
  1817. * @returns a promise
  1818. * @see https://www.w3.org/TR/cssom-1/#dom-cssstylesheet-replace
  1819. */
  1820. CSSOM.CSSStyleSheet.prototype.replace = function(text) {
  1821. var _Promise;
  1822. if (this.__globalObject && this.__globalObject['Promise']) {
  1823. _Promise = this.__globalObject['Promise'];
  1824. } else {
  1825. _Promise = Promise;
  1826. }
  1827. var _setTimeout;
  1828. if (this.__globalObject && this.__globalObject['setTimeout']) {
  1829. _setTimeout = this.__globalObject['setTimeout'];
  1830. } else {
  1831. _setTimeout = setTimeout;
  1832. }
  1833. var sheet = this;
  1834. return new _Promise(function (resolve, reject) {
  1835. // If the constructed flag is not set, or the disallow modification flag is set, throw a NotAllowedError DOMException.
  1836. if (!sheet.__constructed || sheet.__disallowModification) {
  1837. reject(errorUtils.createError(sheet, 'DOMException',
  1838. "Failed to execute 'replaceSync' on '" + sheet.constructor.name + "': Not allowed.",
  1839. 'NotAllowedError'));
  1840. }
  1841. // Set the disallow modification flag.
  1842. sheet.__disallowModification = true;
  1843. // In parallel, do these steps:
  1844. _setTimeout(function() {
  1845. // Let rules be the result of running parse a stylesheet's contents from text.
  1846. var rules = new CSSOM.CSSRuleList();
  1847. CSSOM.parse(text, { styleSheet: sheet, cssRules: rules });
  1848. // If rules contains one or more @import rules, remove those rules from rules.
  1849. var i = 0;
  1850. while (i < rules.length) {
  1851. if (rules[i].constructor.name === 'CSSImportRule') {
  1852. rules.splice(i, 1);
  1853. } else {
  1854. i++;
  1855. }
  1856. }
  1857. // Set sheet's CSS rules to rules.
  1858. sheet.__cssRules.splice.apply(sheet.__cssRules, [0, sheet.__cssRules.length].concat(rules));
  1859. // Unset sheet’s disallow modification flag.
  1860. delete sheet.__disallowModification;
  1861. // Resolve promise with sheet.
  1862. resolve(sheet);
  1863. })
  1864. });
  1865. }
  1866. /**
  1867. * Synchronously replaces the rules of a {@link CSSStyleSheet}
  1868. *
  1869. * @see https://www.w3.org/TR/cssom-1/#dom-cssstylesheet-replacesync
  1870. */
  1871. CSSOM.CSSStyleSheet.prototype.replaceSync = function(text) {
  1872. var sheet = this;
  1873. // If the constructed flag is not set, or the disallow modification flag is set, throw a NotAllowedError DOMException.
  1874. if (!sheet.__constructed || sheet.__disallowModification) {
  1875. errorUtils.throwError(sheet, 'DOMException',
  1876. "Failed to execute 'replaceSync' on '" + sheet.constructor.name + "': Not allowed.",
  1877. 'NotAllowedError');
  1878. }
  1879. // Let rules be the result of running parse a stylesheet's contents from text.
  1880. var rules = new CSSOM.CSSRuleList();
  1881. CSSOM.parse(text, { styleSheet: sheet, cssRules: rules });
  1882. // If rules contains one or more @import rules, remove those rules from rules.
  1883. var i = 0;
  1884. while (i < rules.length) {
  1885. if (rules[i].constructor.name === 'CSSImportRule') {
  1886. rules.splice(i, 1);
  1887. } else {
  1888. i++;
  1889. }
  1890. }
  1891. // Set sheet's CSS rules to rules.
  1892. sheet.__cssRules.splice.apply(sheet.__cssRules, [0, sheet.__cssRules.length].concat(rules));
  1893. }
  1894. /**
  1895. * NON-STANDARD
  1896. * @return {string} serialize stylesheet
  1897. */
  1898. CSSOM.CSSStyleSheet.prototype.toString = function() {
  1899. var result = "";
  1900. var rules = this.cssRules;
  1901. for (var i=0; i<rules.length; i++) {
  1902. result += rules[i].cssText + "\n";
  1903. }
  1904. return result;
  1905. };
  1906. /**
  1907. * @constructor
  1908. * @see http://www.w3.org/TR/css3-animations/#DOM-CSSKeyframesRule
  1909. */
  1910. CSSOM.CSSKeyframesRule = function CSSKeyframesRule() {
  1911. CSSOM.CSSRule.call(this);
  1912. this.name = '';
  1913. this.cssRules = new CSSOM.CSSRuleList();
  1914. // Set up initial indexed access
  1915. this._setupIndexedAccess();
  1916. // Override cssRules methods after initial setup, store references as non-enumerable properties
  1917. var self = this;
  1918. var originalPush = this.cssRules.push;
  1919. var originalSplice = this.cssRules.splice;
  1920. // Create non-enumerable method overrides
  1921. Object.defineProperty(this.cssRules, 'push', {
  1922. value: function() {
  1923. var result = originalPush.apply(this, arguments);
  1924. self._setupIndexedAccess();
  1925. return result;
  1926. },
  1927. writable: true,
  1928. enumerable: false,
  1929. configurable: true
  1930. });
  1931. Object.defineProperty(this.cssRules, 'splice', {
  1932. value: function() {
  1933. var result = originalSplice.apply(this, arguments);
  1934. self._setupIndexedAccess();
  1935. return result;
  1936. },
  1937. writable: true,
  1938. enumerable: false,
  1939. configurable: true
  1940. });
  1941. };
  1942. CSSOM.CSSKeyframesRule.prototype = Object.create(CSSOM.CSSRule.prototype);
  1943. CSSOM.CSSKeyframesRule.prototype.constructor = CSSOM.CSSKeyframesRule;
  1944. Object.setPrototypeOf(CSSOM.CSSKeyframesRule, CSSOM.CSSRule);
  1945. Object.defineProperty(CSSOM.CSSKeyframesRule.prototype, "type", {
  1946. value: 7,
  1947. writable: false
  1948. });
  1949. // http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSKeyframesRule.cpp
  1950. Object.defineProperty(CSSOM.CSSKeyframesRule.prototype, "cssText", {
  1951. get: function() {
  1952. var values = "";
  1953. var valuesArr = [" {"];
  1954. if (this.cssRules.length) {
  1955. valuesArr.push(this.cssRules.reduce(function(acc, rule){
  1956. if (rule.cssText !== "") {
  1957. acc.push(rule.cssText);
  1958. }
  1959. return acc;
  1960. }, []).join("\n "));
  1961. }
  1962. values = valuesArr.join("\n ") + "\n}";
  1963. var cssWideKeywords = ['initial', 'inherit', 'revert', 'revert-layer', 'unset', 'none'];
  1964. var processedName = cssWideKeywords.includes(this.name) ? '"' + this.name + '"' : this.name;
  1965. return "@" + (this._vendorPrefix || '') + "keyframes " + processedName + values;
  1966. }
  1967. });
  1968. /**
  1969. * Appends a new keyframe rule to the list of keyframes.
  1970. *
  1971. * @param {string} rule - The keyframe rule string to append (e.g., "50% { opacity: 0.5; }")
  1972. * @see https://www.w3.org/TR/css-animations-1/#dom-csskeyframesrule-appendrule
  1973. */
  1974. CSSOM.CSSKeyframesRule.prototype.appendRule = function appendRule(rule) {
  1975. if (arguments.length === 0) {
  1976. errorUtils.throwMissingArguments(this, 'appendRule', 'CSSKeyframesRule');
  1977. }
  1978. var parsedRule;
  1979. try {
  1980. // Parse the rule string as a keyframe rule
  1981. var tempStyleSheet = CSSOM.parse("@keyframes temp { " + rule + " }");
  1982. if (tempStyleSheet.cssRules.length > 0 && tempStyleSheet.cssRules[0].cssRules.length > 0) {
  1983. parsedRule = tempStyleSheet.cssRules[0].cssRules[0];
  1984. } else {
  1985. throw new Error("Failed to parse keyframe rule");
  1986. }
  1987. } catch (e) {
  1988. errorUtils.throwParseError(this, 'appendRule', 'CSSKeyframesRule', rule);
  1989. }
  1990. parsedRule.__parentRule = this;
  1991. this.cssRules.push(parsedRule);
  1992. };
  1993. /**
  1994. * Deletes a keyframe rule that matches the specified key.
  1995. *
  1996. * @param {string} select - The keyframe selector to delete (e.g., "50%", "from", "to")
  1997. * @see https://www.w3.org/TR/css-animations-1/#dom-csskeyframesrule-deleterule
  1998. */
  1999. CSSOM.CSSKeyframesRule.prototype.deleteRule = function deleteRule(select) {
  2000. if (arguments.length === 0) {
  2001. errorUtils.throwMissingArguments(this, 'deleteRule', 'CSSKeyframesRule');
  2002. }
  2003. var normalizedSelect = this._normalizeKeyText(select);
  2004. for (var i = 0; i < this.cssRules.length; i++) {
  2005. var rule = this.cssRules[i];
  2006. if (this._normalizeKeyText(rule.keyText) === normalizedSelect) {
  2007. rule.__parentRule = null;
  2008. this.cssRules.splice(i, 1);
  2009. return;
  2010. }
  2011. }
  2012. };
  2013. /**
  2014. * Finds and returns the keyframe rule that matches the specified key.
  2015. * When multiple rules have the same key, returns the last one.
  2016. *
  2017. * @param {string} select - The keyframe selector to find (e.g., "50%", "from", "to")
  2018. * @return {CSSKeyframeRule|null} The matching keyframe rule, or null if not found
  2019. * @see https://www.w3.org/TR/css-animations-1/#dom-csskeyframesrule-findrule
  2020. */
  2021. CSSOM.CSSKeyframesRule.prototype.findRule = function findRule(select) {
  2022. if (arguments.length === 0) {
  2023. errorUtils.throwMissingArguments(this, 'findRule', 'CSSKeyframesRule');
  2024. }
  2025. var normalizedSelect = this._normalizeKeyText(select);
  2026. // Iterate backwards to find the last matching rule
  2027. for (var i = this.cssRules.length - 1; i >= 0; i--) {
  2028. var rule = this.cssRules[i];
  2029. if (this._normalizeKeyText(rule.keyText) === normalizedSelect) {
  2030. return rule;
  2031. }
  2032. }
  2033. return null;
  2034. };
  2035. /**
  2036. * Normalizes keyframe selector text for comparison.
  2037. * Handles "from" -> "0%" and "to" -> "100%" conversions and trims whitespace.
  2038. *
  2039. * @private
  2040. * @param {string} keyText - The keyframe selector text to normalize
  2041. * @return {string} The normalized keyframe selector text
  2042. */
  2043. CSSOM.CSSKeyframesRule.prototype._normalizeKeyText = function _normalizeKeyText(keyText) {
  2044. if (!keyText) return '';
  2045. var normalized = keyText.toString().trim().toLowerCase();
  2046. // Convert keywords to percentages for comparison
  2047. if (normalized === 'from') {
  2048. return '0%';
  2049. } else if (normalized === 'to') {
  2050. return '100%';
  2051. }
  2052. return normalized;
  2053. };
  2054. /**
  2055. * Makes CSSKeyframesRule iterable over its cssRules.
  2056. * Allows for...of loops and other iterable methods.
  2057. */
  2058. if (typeof Symbol !== 'undefined' && Symbol.iterator) {
  2059. CSSOM.CSSKeyframesRule.prototype[Symbol.iterator] = function() {
  2060. var index = 0;
  2061. var cssRules = this.cssRules;
  2062. return {
  2063. next: function() {
  2064. if (index < cssRules.length) {
  2065. return { value: cssRules[index++], done: false };
  2066. } else {
  2067. return { done: true };
  2068. }
  2069. }
  2070. };
  2071. };
  2072. }
  2073. /**
  2074. * Adds indexed getters for direct access to cssRules by index.
  2075. * This enables rule[0], rule[1], etc. access patterns.
  2076. * Works in environments where Proxy is not available (like jsdom).
  2077. */
  2078. CSSOM.CSSKeyframesRule.prototype._setupIndexedAccess = function() {
  2079. // Remove any existing indexed properties
  2080. for (var i = 0; i < 1000; i++) { // reasonable upper limit
  2081. if (this.hasOwnProperty(i)) {
  2082. delete this[i];
  2083. } else {
  2084. break;
  2085. }
  2086. }
  2087. // Add indexed getters for current cssRules
  2088. for (var i = 0; i < this.cssRules.length; i++) {
  2089. (function(index) {
  2090. Object.defineProperty(this, index, {
  2091. get: function() {
  2092. return this.cssRules[index];
  2093. },
  2094. enumerable: false,
  2095. configurable: true
  2096. });
  2097. }.call(this, i));
  2098. }
  2099. // Update length property
  2100. Object.defineProperty(this, 'length', {
  2101. get: function() {
  2102. return this.cssRules.length;
  2103. },
  2104. enumerable: false,
  2105. configurable: true
  2106. });
  2107. };
  2108. /**
  2109. * @constructor
  2110. * @see http://www.w3.org/TR/css3-animations/#DOM-CSSKeyframeRule
  2111. */
  2112. CSSOM.CSSKeyframeRule = function CSSKeyframeRule() {
  2113. CSSOM.CSSRule.call(this);
  2114. this.keyText = '';
  2115. this.__style = new CSSOM.CSSStyleDeclaration();
  2116. this.__style.parentRule = this;
  2117. };
  2118. CSSOM.CSSKeyframeRule.prototype = Object.create(CSSOM.CSSRule.prototype);
  2119. CSSOM.CSSKeyframeRule.prototype.constructor = CSSOM.CSSKeyframeRule;
  2120. Object.setPrototypeOf(CSSOM.CSSKeyframeRule, CSSOM.CSSRule);
  2121. Object.defineProperty(CSSOM.CSSKeyframeRule.prototype, "type", {
  2122. value: 8,
  2123. writable: false
  2124. });
  2125. //FIXME
  2126. //CSSOM.CSSKeyframeRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
  2127. //CSSOM.CSSKeyframeRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
  2128. Object.defineProperty(CSSOM.CSSKeyframeRule.prototype, "style", {
  2129. get: function() {
  2130. return this.__style;
  2131. },
  2132. set: function(value) {
  2133. if (typeof value === "string") {
  2134. this.__style.cssText = value;
  2135. } else {
  2136. this.__style = value;
  2137. }
  2138. }
  2139. });
  2140. // http://www.opensource.apple.com/source/WebCore/WebCore-955.66.1/css/WebKitCSSKeyframeRule.cpp
  2141. Object.defineProperty(CSSOM.CSSKeyframeRule.prototype, "cssText", {
  2142. get: function() {
  2143. return this.keyText + " {" + (this.style.cssText ? " " + this.style.cssText : "") + " }";
  2144. }
  2145. });
  2146. /**
  2147. * @constructor
  2148. * @see https://developer.mozilla.org/en/CSS/@-moz-document
  2149. */
  2150. CSSOM.MatcherList = function MatcherList(){
  2151. this.length = 0;
  2152. };
  2153. CSSOM.MatcherList.prototype = {
  2154. constructor: CSSOM.MatcherList,
  2155. /**
  2156. * @return {string}
  2157. */
  2158. get matcherText() {
  2159. return Array.prototype.join.call(this, ", ");
  2160. },
  2161. /**
  2162. * @param {string} value
  2163. */
  2164. set matcherText(value) {
  2165. // just a temporary solution, actually it may be wrong by just split the value with ',', because a url can include ','.
  2166. var values = value.split(",");
  2167. var length = this.length = values.length;
  2168. for (var i=0; i<length; i++) {
  2169. this[i] = values[i].trim();
  2170. }
  2171. },
  2172. /**
  2173. * @param {string} matcher
  2174. */
  2175. appendMatcher: function(matcher) {
  2176. if (Array.prototype.indexOf.call(this, matcher) === -1) {
  2177. this[this.length] = matcher;
  2178. this.length++;
  2179. }
  2180. },
  2181. /**
  2182. * @param {string} matcher
  2183. */
  2184. deleteMatcher: function(matcher) {
  2185. var index = Array.prototype.indexOf.call(this, matcher);
  2186. if (index !== -1) {
  2187. Array.prototype.splice.call(this, index, 1);
  2188. }
  2189. }
  2190. };
  2191. /**
  2192. * @constructor
  2193. * @see https://developer.mozilla.org/en/CSS/@-moz-document
  2194. * @deprecated This rule is a non-standard Mozilla-specific extension and is not part of any official CSS specification.
  2195. */
  2196. CSSOM.CSSDocumentRule = function CSSDocumentRule() {
  2197. CSSOM.CSSRule.call(this);
  2198. this.matcher = new CSSOM.MatcherList();
  2199. this.cssRules = new CSSOM.CSSRuleList();
  2200. };
  2201. CSSOM.CSSDocumentRule.prototype = Object.create(CSSOM.CSSRule.prototype);
  2202. CSSOM.CSSDocumentRule.prototype.constructor = CSSOM.CSSDocumentRule;
  2203. Object.setPrototypeOf(CSSOM.CSSDocumentRule, CSSOM.CSSRule);
  2204. Object.defineProperty(CSSOM.CSSDocumentRule.prototype, "type", {
  2205. value: 10,
  2206. writable: false
  2207. });
  2208. //FIXME
  2209. //CSSOM.CSSDocumentRule.prototype.insertRule = CSSStyleSheet.prototype.insertRule;
  2210. //CSSOM.CSSDocumentRule.prototype.deleteRule = CSSStyleSheet.prototype.deleteRule;
  2211. Object.defineProperty(CSSOM.CSSDocumentRule.prototype, "cssText", {
  2212. get: function() {
  2213. var cssTexts = [];
  2214. for (var i=0, length=this.cssRules.length; i < length; i++) {
  2215. cssTexts.push(this.cssRules[i].cssText);
  2216. }
  2217. return "@-moz-document " + this.matcher.matcherText + " {" + (cssTexts.length ? "\n " + cssTexts.join("\n ") : "") + "\n}";
  2218. }
  2219. });
  2220. /**
  2221. * @constructor
  2222. * @see http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSValue
  2223. *
  2224. * TODO: add if needed
  2225. */
  2226. CSSOM.CSSValue = function CSSValue() {
  2227. };
  2228. CSSOM.CSSValue.prototype = {
  2229. constructor: CSSOM.CSSValue,
  2230. // @see: http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSValue
  2231. set cssText(text) {
  2232. var name = this._getConstructorName();
  2233. throw new Error('DOMException: property "cssText" of "' + name + '" is readonly and can not be replaced with "' + text + '"!');
  2234. },
  2235. get cssText() {
  2236. var name = this._getConstructorName();
  2237. throw new Error('getter "cssText" of "' + name + '" is not implemented!');
  2238. },
  2239. _getConstructorName: function() {
  2240. var s = this.constructor.toString(),
  2241. c = s.match(/function\s([^\(]+)/),
  2242. name = c[1];
  2243. return name;
  2244. }
  2245. };
  2246. /**
  2247. * @constructor
  2248. * @see http://msdn.microsoft.com/en-us/library/ms537634(v=vs.85).aspx
  2249. *
  2250. */
  2251. CSSOM.CSSValueExpression = function CSSValueExpression(token, idx) {
  2252. this._token = token;
  2253. this._idx = idx;
  2254. };
  2255. CSSOM.CSSValueExpression.prototype = Object.create(CSSOM.CSSValue.prototype);
  2256. CSSOM.CSSValueExpression.prototype.constructor = CSSOM.CSSValueExpression;
  2257. Object.setPrototypeOf(CSSOM.CSSValueExpression, CSSOM.CSSValue);
  2258. /**
  2259. * parse css expression() value
  2260. *
  2261. * @return {Object}
  2262. * - error:
  2263. * or
  2264. * - idx:
  2265. * - expression:
  2266. *
  2267. * Example:
  2268. *
  2269. * .selector {
  2270. * zoom: expression(documentElement.clientWidth > 1000 ? '1000px' : 'auto');
  2271. * }
  2272. */
  2273. CSSOM.CSSValueExpression.prototype.parse = function() {
  2274. var token = this._token,
  2275. idx = this._idx;
  2276. var character = '',
  2277. expression = '',
  2278. error = '',
  2279. info,
  2280. paren = [];
  2281. for (; ; ++idx) {
  2282. character = token.charAt(idx);
  2283. // end of token
  2284. if (character === '') {
  2285. error = 'css expression error: unfinished expression!';
  2286. break;
  2287. }
  2288. switch(character) {
  2289. case '(':
  2290. paren.push(character);
  2291. expression += character;
  2292. break;
  2293. case ')':
  2294. paren.pop(character);
  2295. expression += character;
  2296. break;
  2297. case '/':
  2298. if ((info = this._parseJSComment(token, idx))) { // comment?
  2299. if (info.error) {
  2300. error = 'css expression error: unfinished comment in expression!';
  2301. } else {
  2302. idx = info.idx;
  2303. // ignore the comment
  2304. }
  2305. } else if ((info = this._parseJSRexExp(token, idx))) { // regexp
  2306. idx = info.idx;
  2307. expression += info.text;
  2308. } else { // other
  2309. expression += character;
  2310. }
  2311. break;
  2312. case "'":
  2313. case '"':
  2314. info = this._parseJSString(token, idx, character);
  2315. if (info) { // string
  2316. idx = info.idx;
  2317. expression += info.text;
  2318. } else {
  2319. expression += character;
  2320. }
  2321. break;
  2322. default:
  2323. expression += character;
  2324. break;
  2325. }
  2326. if (error) {
  2327. break;
  2328. }
  2329. // end of expression
  2330. if (paren.length === 0) {
  2331. break;
  2332. }
  2333. }
  2334. var ret;
  2335. if (error) {
  2336. ret = {
  2337. error: error
  2338. };
  2339. } else {
  2340. ret = {
  2341. idx: idx,
  2342. expression: expression
  2343. };
  2344. }
  2345. return ret;
  2346. };
  2347. /**
  2348. *
  2349. * @return {Object|false}
  2350. * - idx:
  2351. * - text:
  2352. * or
  2353. * - error:
  2354. * or
  2355. * false
  2356. *
  2357. */
  2358. CSSOM.CSSValueExpression.prototype._parseJSComment = function(token, idx) {
  2359. var nextChar = token.charAt(idx + 1),
  2360. text;
  2361. if (nextChar === '/' || nextChar === '*') {
  2362. var startIdx = idx,
  2363. endIdx,
  2364. commentEndChar;
  2365. if (nextChar === '/') { // line comment
  2366. commentEndChar = '\n';
  2367. } else if (nextChar === '*') { // block comment
  2368. commentEndChar = '*/';
  2369. }
  2370. endIdx = token.indexOf(commentEndChar, startIdx + 1 + 1);
  2371. if (endIdx !== -1) {
  2372. endIdx = endIdx + commentEndChar.length - 1;
  2373. text = token.substring(idx, endIdx + 1);
  2374. return {
  2375. idx: endIdx,
  2376. text: text
  2377. };
  2378. } else {
  2379. var error = 'css expression error: unfinished comment in expression!';
  2380. return {
  2381. error: error
  2382. };
  2383. }
  2384. } else {
  2385. return false;
  2386. }
  2387. };
  2388. /**
  2389. *
  2390. * @return {Object|false}
  2391. * - idx:
  2392. * - text:
  2393. * or
  2394. * false
  2395. *
  2396. */
  2397. CSSOM.CSSValueExpression.prototype._parseJSString = function(token, idx, sep) {
  2398. var endIdx = this._findMatchedIdx(token, idx, sep),
  2399. text;
  2400. if (endIdx === -1) {
  2401. return false;
  2402. } else {
  2403. text = token.substring(idx, endIdx + sep.length);
  2404. return {
  2405. idx: endIdx,
  2406. text: text
  2407. };
  2408. }
  2409. };
  2410. /**
  2411. * parse regexp in css expression
  2412. *
  2413. * @return {Object|false}
  2414. * - idx:
  2415. * - regExp:
  2416. * or
  2417. * false
  2418. */
  2419. /*
  2420. all legal RegExp
  2421. /a/
  2422. (/a/)
  2423. [/a/]
  2424. [12, /a/]
  2425. !/a/
  2426. +/a/
  2427. -/a/
  2428. * /a/
  2429. / /a/
  2430. %/a/
  2431. ===/a/
  2432. !==/a/
  2433. ==/a/
  2434. !=/a/
  2435. >/a/
  2436. >=/a/
  2437. </a/
  2438. <=/a/
  2439. &/a/
  2440. |/a/
  2441. ^/a/
  2442. ~/a/
  2443. <</a/
  2444. >>/a/
  2445. >>>/a/
  2446. &&/a/
  2447. ||/a/
  2448. ?/a/
  2449. =/a/
  2450. ,/a/
  2451. delete /a/
  2452. in /a/
  2453. instanceof /a/
  2454. new /a/
  2455. typeof /a/
  2456. void /a/
  2457. */
  2458. CSSOM.CSSValueExpression.prototype._parseJSRexExp = function(token, idx) {
  2459. var before = token.substring(0, idx).replace(/\s+$/, ""),
  2460. legalRegx = [
  2461. /^$/,
  2462. /\($/,
  2463. /\[$/,
  2464. /\!$/,
  2465. /\+$/,
  2466. /\-$/,
  2467. /\*$/,
  2468. /\/\s+/,
  2469. /\%$/,
  2470. /\=$/,
  2471. /\>$/,
  2472. /<$/,
  2473. /\&$/,
  2474. /\|$/,
  2475. /\^$/,
  2476. /\~$/,
  2477. /\?$/,
  2478. /\,$/,
  2479. /delete$/,
  2480. /in$/,
  2481. /instanceof$/,
  2482. /new$/,
  2483. /typeof$/,
  2484. /void$/
  2485. ];
  2486. var isLegal = legalRegx.some(function(reg) {
  2487. return reg.test(before);
  2488. });
  2489. if (!isLegal) {
  2490. return false;
  2491. } else {
  2492. var sep = '/';
  2493. // same logic as string
  2494. return this._parseJSString(token, idx, sep);
  2495. }
  2496. };
  2497. /**
  2498. *
  2499. * find next sep(same line) index in `token`
  2500. *
  2501. * @return {Number}
  2502. *
  2503. */
  2504. CSSOM.CSSValueExpression.prototype._findMatchedIdx = function(token, idx, sep) {
  2505. var startIdx = idx,
  2506. endIdx;
  2507. var NOT_FOUND = -1;
  2508. while(true) {
  2509. endIdx = token.indexOf(sep, startIdx + 1);
  2510. if (endIdx === -1) { // not found
  2511. endIdx = NOT_FOUND;
  2512. break;
  2513. } else {
  2514. var text = token.substring(idx + 1, endIdx),
  2515. matched = text.match(/\\+$/);
  2516. if (!matched || matched[0] % 2 === 0) { // not escaped
  2517. break;
  2518. } else {
  2519. startIdx = endIdx;
  2520. }
  2521. }
  2522. }
  2523. // boundary must be in the same line(js sting or regexp)
  2524. var nextNewLineIdx = token.indexOf('\n', idx + 1);
  2525. if (nextNewLineIdx < endIdx) {
  2526. endIdx = NOT_FOUND;
  2527. }
  2528. return endIdx;
  2529. };
  2530. /**
  2531. * @constructor
  2532. * @see https://drafts.csswg.org/css-cascade-6/#cssscoperule
  2533. */
  2534. CSSOM.CSSScopeRule = function CSSScopeRule() {
  2535. CSSOM.CSSGroupingRule.call(this);
  2536. this.__start = null;
  2537. this.__end = null;
  2538. };
  2539. CSSOM.CSSScopeRule.prototype = Object.create(CSSOM.CSSGroupingRule.prototype);
  2540. CSSOM.CSSScopeRule.prototype.constructor = CSSOM.CSSScopeRule;
  2541. Object.setPrototypeOf(CSSOM.CSSScopeRule, CSSOM.CSSGroupingRule);
  2542. Object.defineProperties(CSSOM.CSSScopeRule.prototype, {
  2543. type: {
  2544. value: 0,
  2545. writable: false,
  2546. },
  2547. cssText: {
  2548. get: function () {
  2549. var values = "";
  2550. var valuesArr = [" {"];
  2551. if (this.cssRules.length) {
  2552. valuesArr.push(this.cssRules.reduce(function(acc, rule){
  2553. if (rule.cssText !== "") {
  2554. acc.push(rule.cssText);
  2555. }
  2556. return acc;
  2557. }, []).join("\n "));
  2558. }
  2559. values = valuesArr.join("\n ") + "\n}";
  2560. return "@scope" + (this.start ? " (" + this.start + ")" : "") + (this.end ? " to (" + this.end + ")" : "") + values;
  2561. },
  2562. configurable: true,
  2563. enumerable: true,
  2564. },
  2565. start: {
  2566. get: function () {
  2567. return this.__start;
  2568. }
  2569. },
  2570. end: {
  2571. get: function () {
  2572. return this.__end;
  2573. }
  2574. }
  2575. });
  2576. /**
  2577. * @constructor
  2578. * @see https://drafts.csswg.org/css-cascade-5/#csslayerblockrule
  2579. */
  2580. CSSOM.CSSLayerBlockRule = function CSSLayerBlockRule() {
  2581. CSSOM.CSSGroupingRule.call(this);
  2582. this.name = "";
  2583. };
  2584. CSSOM.CSSLayerBlockRule.prototype = Object.create(CSSOM.CSSGroupingRule.prototype);
  2585. CSSOM.CSSLayerBlockRule.prototype.constructor = CSSOM.CSSLayerBlockRule;
  2586. Object.setPrototypeOf(CSSOM.CSSLayerBlockRule, CSSOM.CSSRule);
  2587. Object.defineProperty(CSSOM.CSSLayerBlockRule.prototype, "type", {
  2588. value: 18,
  2589. writable: false
  2590. });
  2591. Object.defineProperties(CSSOM.CSSLayerBlockRule.prototype, {
  2592. cssText: {
  2593. get: function () {
  2594. var values = "";
  2595. var valuesArr = [" {"];
  2596. if (this.cssRules.length) {
  2597. valuesArr.push(this.cssRules.reduce(function(acc, rule){
  2598. if (rule.cssText !== "") {
  2599. acc.push(rule.cssText);
  2600. }
  2601. return acc;
  2602. }, []).join("\n "));
  2603. }
  2604. values = valuesArr.join("\n ") + "\n}";
  2605. return "@layer" + (this.name ? " " + this.name : "") + values;
  2606. }
  2607. },
  2608. });
  2609. /**
  2610. * @constructor
  2611. * @see https://drafts.csswg.org/css-cascade-5/#csslayerstatementrule
  2612. */
  2613. CSSOM.CSSLayerStatementRule = function CSSLayerStatementRule() {
  2614. CSSOM.CSSRule.call(this);
  2615. this.nameList = [];
  2616. };
  2617. CSSOM.CSSLayerStatementRule.prototype = Object.create(CSSOM.CSSRule.prototype);
  2618. CSSOM.CSSLayerStatementRule.prototype.constructor = CSSOM.CSSLayerStatementRule;
  2619. Object.setPrototypeOf(CSSOM.CSSLayerStatementRule, CSSOM.CSSRule);
  2620. Object.defineProperty(CSSOM.CSSLayerStatementRule.prototype, "type", {
  2621. value: 0,
  2622. writable: false
  2623. });
  2624. Object.defineProperties(CSSOM.CSSLayerStatementRule.prototype, {
  2625. cssText: {
  2626. get: function () {
  2627. return "@layer " + this.nameList.join(", ") + ";";
  2628. }
  2629. },
  2630. });
  2631. /**
  2632. * @constructor
  2633. * @see https://drafts.csswg.org/cssom/#the-csspagerule-interface
  2634. */
  2635. CSSOM.CSSPageRule = function CSSPageRule() {
  2636. CSSOM.CSSGroupingRule.call(this);
  2637. this.__style = new CSSOM.CSSStyleDeclaration();
  2638. this.__style.parentRule = this;
  2639. };
  2640. CSSOM.CSSPageRule.prototype = Object.create(CSSOM.CSSGroupingRule.prototype);
  2641. CSSOM.CSSPageRule.prototype.constructor = CSSOM.CSSPageRule;
  2642. Object.setPrototypeOf(CSSOM.CSSPageRule, CSSOM.CSSGroupingRule);
  2643. Object.defineProperty(CSSOM.CSSPageRule.prototype, "type", {
  2644. value: 6,
  2645. writable: false
  2646. });
  2647. Object.defineProperty(CSSOM.CSSPageRule.prototype, "selectorText", {
  2648. get: function() {
  2649. return this.__selectorText;
  2650. },
  2651. set: function(value) {
  2652. if (typeof value === "string") {
  2653. var trimmedValue = value.trim();
  2654. // Empty selector is valid for @page
  2655. if (trimmedValue === '') {
  2656. this.__selectorText = '';
  2657. return;
  2658. }
  2659. var atPageRuleSelectorRegExp = regexPatterns.atPageRuleSelectorRegExp;
  2660. var cssCustomIdentifierRegExp = regexPatterns.cssCustomIdentifierRegExp;
  2661. var match = trimmedValue.match(atPageRuleSelectorRegExp);
  2662. if (match) {
  2663. var pageName = match[1] || '';
  2664. var pseudoPages = match[2] || '';
  2665. // Validate page name if present
  2666. if (pageName) {
  2667. // Page name can be an identifier or a string
  2668. if (!cssCustomIdentifierRegExp.test(pageName)) {
  2669. return;
  2670. }
  2671. }
  2672. // Validate pseudo-pages if present
  2673. if (pseudoPages) {
  2674. var pseudos = pseudoPages.split(':').filter(function(p) { return p; });
  2675. var validPseudos = ['left', 'right', 'first', 'blank'];
  2676. var allValid = true;
  2677. for (var j = 0; j < pseudos.length; j++) {
  2678. if (validPseudos.indexOf(pseudos[j].toLowerCase()) === -1) {
  2679. allValid = false;
  2680. break;
  2681. }
  2682. }
  2683. if (!allValid) {
  2684. return; // Invalid pseudo-page, do nothing
  2685. }
  2686. }
  2687. this.__selectorText = pageName + pseudoPages.toLowerCase();
  2688. }
  2689. }
  2690. }
  2691. });
  2692. Object.defineProperty(CSSOM.CSSPageRule.prototype, "style", {
  2693. get: function() {
  2694. return this.__style;
  2695. },
  2696. set: function(value) {
  2697. if (typeof value === "string") {
  2698. this.__style.cssText = value;
  2699. } else {
  2700. this.__style = value;
  2701. }
  2702. }
  2703. });
  2704. Object.defineProperty(CSSOM.CSSPageRule.prototype, "cssText", {
  2705. get: function() {
  2706. var values = "";
  2707. if (this.cssRules.length) {
  2708. var valuesArr = [" {"];
  2709. this.style.cssText && valuesArr.push(this.style.cssText);
  2710. valuesArr.push(this.cssRules.reduce(function(acc, rule){
  2711. if (rule.cssText !== "") {
  2712. acc.push(rule.cssText);
  2713. }
  2714. return acc;
  2715. }, []).join("\n "));
  2716. values = valuesArr.join("\n ") + "\n}";
  2717. } else {
  2718. values = " {" + (this.style.cssText ? " " + this.style.cssText : "") + " }";
  2719. }
  2720. return "@page" + (this.selectorText ? " " + this.selectorText : "") + values;
  2721. }
  2722. });
  2723. /**
  2724. * Parses a CSS string and returns a `CSSStyleSheet` object representing the parsed stylesheet.
  2725. *
  2726. * @param {string} token - The CSS string to parse.
  2727. * @param {object} [opts] - Optional parsing options.
  2728. * @param {object} [opts.globalObject] - An optional global object to prioritize over the window object. Useful on jsdom webplatform tests.
  2729. * @param {Element | ProcessingInstruction} [opts.ownerNode] - The owner node of the stylesheet.
  2730. * @param {CSSRule} [opts.ownerRule] - The owner rule of the stylesheet.
  2731. * @param {CSSOM.CSSStyleSheet} [opts.styleSheet] - Reuse a style sheet instead of creating a new one (e.g. as `parentStyleSheet`)
  2732. * @param {CSSOM.CSSRuleList} [opts.cssRules] - Prepare all rules in this list instead of mutating the style sheet continually
  2733. * @param {function|boolean} [errorHandler] - Optional error handler function or `true` to use `console.error`.
  2734. * @returns {CSSOM.CSSStyleSheet} The parsed `CSSStyleSheet` object.
  2735. */
  2736. CSSOM.parse = function parse(token, opts, errorHandler) {
  2737. errorHandler = errorHandler === true ? (console && console.error) : errorHandler;
  2738. var i = 0;
  2739. /**
  2740. "before-selector" or
  2741. "selector" or
  2742. "atRule" or
  2743. "atBlock" or
  2744. "conditionBlock" or
  2745. "before-name" or
  2746. "name" or
  2747. "before-value" or
  2748. "value"
  2749. */
  2750. var state = "before-selector";
  2751. var index;
  2752. var buffer = "";
  2753. var valueParenthesisDepth = 0;
  2754. var hasUnmatchedQuoteInSelector = false; // Track if current selector has unmatched quote
  2755. var SIGNIFICANT_WHITESPACE = {
  2756. "name": true,
  2757. "before-name": true,
  2758. "selector": true,
  2759. "value": true,
  2760. "value-parenthesis": true,
  2761. "atRule": true,
  2762. "importRule-begin": true,
  2763. "importRule": true,
  2764. "namespaceRule-begin": true,
  2765. "namespaceRule": true,
  2766. "atBlock": true,
  2767. "containerBlock": true,
  2768. "conditionBlock": true,
  2769. "counterStyleBlock": true,
  2770. "propertyBlock": true,
  2771. 'documentRule-begin': true,
  2772. "scopeBlock": true,
  2773. "layerBlock": true,
  2774. "pageBlock": true
  2775. };
  2776. var styleSheet;
  2777. if (opts && opts.styleSheet) {
  2778. styleSheet = opts.styleSheet;
  2779. } else {
  2780. if (opts && opts.globalObject && opts.globalObject.CSSStyleSheet) {
  2781. styleSheet = new opts.globalObject.CSSStyleSheet();
  2782. } else {
  2783. styleSheet = new CSSOM.CSSStyleSheet();
  2784. }
  2785. styleSheet.__constructed = false;
  2786. }
  2787. var topScope;
  2788. if (opts && opts.cssRules) {
  2789. topScope = { cssRules: opts.cssRules };
  2790. } else {
  2791. topScope = styleSheet;
  2792. }
  2793. if (opts && opts.ownerNode) {
  2794. styleSheet.__ownerNode = opts.ownerNode;
  2795. var ownerNodeMedia = opts.ownerNode.media || (opts.ownerNode.getAttribute && opts.ownerNode.getAttribute("media"));
  2796. if (ownerNodeMedia) {
  2797. styleSheet.media.mediaText = ownerNodeMedia;
  2798. }
  2799. var ownerNodeTitle = opts.ownerNode.title || (opts.ownerNode.getAttribute && opts.ownerNode.getAttribute("title"));
  2800. if (ownerNodeTitle) {
  2801. styleSheet.__title = ownerNodeTitle;
  2802. }
  2803. }
  2804. if (opts && opts.ownerRule) {
  2805. styleSheet.__ownerRule = opts.ownerRule;
  2806. }
  2807. // @type CSSStyleSheet|CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSFontFaceRule|CSSKeyframesRule|CSSDocumentRule
  2808. var currentScope = topScope;
  2809. // @type CSSMediaRule|CSSContainerRule|CSSSupportsRule|CSSKeyframesRule|CSSDocumentRule
  2810. var parentRule;
  2811. var ancestorRules = [];
  2812. var prevScope;
  2813. var name, priority = "", styleRule, mediaRule, containerRule, counterStyleRule, propertyRule, supportsRule, importRule, fontFaceRule, keyframesRule, documentRule, hostRule, startingStyleRule, scopeRule, pageRule, layerBlockRule, layerStatementRule, nestedSelectorRule, namespaceRule;
  2814. // Track defined namespace prefixes for validation
  2815. var definedNamespacePrefixes = {};
  2816. // Track which rules have been added
  2817. var ruleIdCounter = 0;
  2818. var addedToParent = {};
  2819. var addedToTopScope = {};
  2820. var addedToCurrentScope = {};
  2821. // Helper to get unique ID for tracking rules
  2822. function getRuleId(rule) {
  2823. if (!rule.__parseId) {
  2824. rule.__parseId = ++ruleIdCounter;
  2825. }
  2826. return rule.__parseId;
  2827. }
  2828. // Cache last validation boundary position
  2829. // to avoid rescanning the entire token string for each at-rule
  2830. var lastValidationBoundary = 0;
  2831. // Pre-compile validation regexes for common at-rules
  2832. var validationRegexCache = {};
  2833. function getValidationRegex(atRuleKey) {
  2834. if (!validationRegexCache[atRuleKey]) {
  2835. var sourceRuleRegExp = atRuleKey === "@import" ? forwardImportRuleValidationRegExp : forwardRuleValidationRegExp;
  2836. validationRegexCache[atRuleKey] = new RegExp(atRuleKey + sourceRuleRegExp.source, sourceRuleRegExp.flags);
  2837. }
  2838. return validationRegexCache[atRuleKey];
  2839. }
  2840. // Import regex patterns from shared module
  2841. var atKeyframesRegExp = regexPatterns.atKeyframesRegExp;
  2842. var beforeRulePortionRegExp = regexPatterns.beforeRulePortionRegExp;
  2843. var beforeRuleValidationRegExp = regexPatterns.beforeRuleValidationRegExp;
  2844. var forwardRuleValidationRegExp = regexPatterns.forwardRuleValidationRegExp;
  2845. var forwardImportRuleValidationRegExp = regexPatterns.forwardImportRuleValidationRegExp;
  2846. // Pre-compile regexBefore to avoid creating it on every validateAtRule call
  2847. var regexBefore = new RegExp(beforeRulePortionRegExp.source, beforeRulePortionRegExp.flags);
  2848. var forwardRuleClosingBraceRegExp = regexPatterns.forwardRuleClosingBraceRegExp;
  2849. var forwardRuleSemicolonAndOpeningBraceRegExp = regexPatterns.forwardRuleSemicolonAndOpeningBraceRegExp;
  2850. var cssCustomIdentifierRegExp = regexPatterns.cssCustomIdentifierRegExp;
  2851. var startsWithCombinatorRegExp = regexPatterns.startsWithCombinatorRegExp;
  2852. var atPageRuleSelectorRegExp = regexPatterns.atPageRuleSelectorRegExp;
  2853. var startsWithHexEscapeRegExp = regexPatterns.startsWithHexEscapeRegExp;
  2854. var identStartCharRegExp = regexPatterns.identStartCharRegExp;
  2855. var identCharRegExp = regexPatterns.identCharRegExp;
  2856. var specialCharsNeedEscapeRegExp = regexPatterns.specialCharsNeedEscapeRegExp;
  2857. var combinatorOrSeparatorRegExp = regexPatterns.combinatorOrSeparatorRegExp;
  2858. var afterHexEscapeSeparatorRegExp = regexPatterns.afterHexEscapeSeparatorRegExp;
  2859. var trailingSpaceSeparatorRegExp = regexPatterns.trailingSpaceSeparatorRegExp;
  2860. var endsWithHexEscapeRegExp = regexPatterns.endsWithHexEscapeRegExp;
  2861. var attributeSelectorContentRegExp = regexPatterns.attributeSelectorContentRegExp;
  2862. var pseudoElementRegExp = regexPatterns.pseudoElementRegExp;
  2863. var invalidCombinatorLtGtRegExp = regexPatterns.invalidCombinatorLtGtRegExp;
  2864. var invalidCombinatorDoubleGtRegExp = regexPatterns.invalidCombinatorDoubleGtRegExp;
  2865. var consecutiveCombinatorsRegExp = regexPatterns.consecutiveCombinatorsRegExp;
  2866. var invalidSlottedRegExp = regexPatterns.invalidSlottedRegExp;
  2867. var invalidPartRegExp = regexPatterns.invalidPartRegExp;
  2868. var invalidCueRegExp = regexPatterns.invalidCueRegExp;
  2869. var invalidCueRegionRegExp = regexPatterns.invalidCueRegionRegExp;
  2870. var invalidNestingPattern = regexPatterns.invalidNestingPattern;
  2871. var emptyPseudoClassRegExp = regexPatterns.emptyPseudoClassRegExp;
  2872. var whitespaceNormalizationRegExp = regexPatterns.whitespaceNormalizationRegExp;
  2873. var newlineRemovalRegExp = regexPatterns.newlineRemovalRegExp;
  2874. var whitespaceAndDotRegExp = regexPatterns.whitespaceAndDotRegExp;
  2875. var declarationOrOpenBraceRegExp = regexPatterns.declarationOrOpenBraceRegExp;
  2876. var ampersandRegExp = regexPatterns.ampersandRegExp;
  2877. var hexEscapeSequenceRegExp = regexPatterns.hexEscapeSequenceRegExp;
  2878. var attributeCaseFlagRegExp = regexPatterns.attributeCaseFlagRegExp;
  2879. var prependedAmpersandRegExp = regexPatterns.prependedAmpersandRegExp;
  2880. var openBraceGlobalRegExp = regexPatterns.openBraceGlobalRegExp;
  2881. var closeBraceGlobalRegExp = regexPatterns.closeBraceGlobalRegExp;
  2882. var scopePreludeSplitRegExp = regexPatterns.scopePreludeSplitRegExp;
  2883. var leadingWhitespaceRegExp = regexPatterns.leadingWhitespaceRegExp;
  2884. var doubleQuoteRegExp = regexPatterns.doubleQuoteRegExp;
  2885. var backslashRegExp = regexPatterns.backslashRegExp;
  2886. /**
  2887. * Searches for the first occurrence of a CSS at-rule statement terminator (`;` or `}`)
  2888. * that is not inside a brace block within the given string. Mimics the behavior of a
  2889. * regular expression match for such terminators, including any trailing whitespace.
  2890. * @param {string} str - The string to search for at-rule statement terminators.
  2891. * @returns {object | null} {0: string, index: number} or null if no match is found.
  2892. */
  2893. function atRulesStatemenRegExpES5Alternative(ruleSlice) {
  2894. for (var i = 0; i < ruleSlice.length; i++) {
  2895. var char = ruleSlice[i];
  2896. if (char === ';' || char === '}') {
  2897. // Simulate negative lookbehind: check if there is a { before this position
  2898. var sliceBefore = ruleSlice.substring(0, i);
  2899. var openBraceIndex = sliceBefore.indexOf('{');
  2900. if (openBraceIndex === -1) {
  2901. // No { found before, so we treat it as a valid match
  2902. var match = char;
  2903. var j = i + 1;
  2904. while (j < ruleSlice.length && /\s/.test(ruleSlice[j])) {
  2905. match += ruleSlice[j];
  2906. j++;
  2907. }
  2908. var matchObj = [match];
  2909. matchObj.index = i;
  2910. matchObj.input = ruleSlice;
  2911. return matchObj;
  2912. }
  2913. }
  2914. }
  2915. return null;
  2916. }
  2917. /**
  2918. * Finds the first balanced block (including nested braces) in the string, starting from fromIndex.
  2919. * Returns an object similar to RegExp.prototype.match output.
  2920. * @param {string} str - The string to search.
  2921. * @param {number} [fromIndex=0] - The index to start searching from.
  2922. * @returns {object|null} - { 0: matchedString, index: startIndex, input: str } or null if not found.
  2923. */
  2924. function matchBalancedBlock(str, fromIndex) {
  2925. fromIndex = fromIndex || 0;
  2926. var openIndex = str.indexOf('{', fromIndex);
  2927. if (openIndex === -1) return null;
  2928. var depth = 0;
  2929. for (var i = openIndex; i < str.length; i++) {
  2930. if (str[i] === '{') {
  2931. depth++;
  2932. } else if (str[i] === '}') {
  2933. depth--;
  2934. if (depth === 0) {
  2935. var matchedString = str.slice(openIndex, i + 1);
  2936. return {
  2937. 0: matchedString,
  2938. index: openIndex,
  2939. input: str
  2940. };
  2941. }
  2942. }
  2943. }
  2944. return null;
  2945. }
  2946. /**
  2947. * Advances the index `i` to skip over a balanced block of curly braces in the given string.
  2948. * This is typically used to ignore the contents of a CSS rule block.
  2949. *
  2950. * @param {number} i - The current index in the string to start searching from.
  2951. * @param {string} str - The string containing the CSS code.
  2952. * @param {number} fromIndex - The index in the string where the balanced block search should begin.
  2953. * @returns {number} The updated index after skipping the balanced block.
  2954. */
  2955. function ignoreBalancedBlock(i, str, fromIndex) {
  2956. var ruleClosingMatch = matchBalancedBlock(str, fromIndex);
  2957. if (ruleClosingMatch) {
  2958. var ignoreRange = ruleClosingMatch.index + ruleClosingMatch[0].length;
  2959. i += ignoreRange;
  2960. if (token.charAt(i) === '}') {
  2961. i -= 1;
  2962. }
  2963. } else {
  2964. i += str.length;
  2965. }
  2966. return i;
  2967. }
  2968. /**
  2969. * Parses the scope prelude and extracts start and end selectors.
  2970. * @param {string} preludeContent - The scope prelude content (without @scope keyword)
  2971. * @returns {object} Object with startSelector and endSelector properties
  2972. */
  2973. function parseScopePrelude(preludeContent) {
  2974. var parts = preludeContent.split(scopePreludeSplitRegExp);
  2975. // Restore the parentheses that were consumed by the split
  2976. if (parts.length === 2) {
  2977. parts[0] = parts[0] + ')';
  2978. parts[1] = '(' + parts[1];
  2979. }
  2980. var hasStart = parts[0] &&
  2981. parts[0].charAt(0) === '(' &&
  2982. parts[0].charAt(parts[0].length - 1) === ')';
  2983. var hasEnd = parts[1] &&
  2984. parts[1].charAt(0) === '(' &&
  2985. parts[1].charAt(parts[1].length - 1) === ')';
  2986. // Handle case: @scope to (<end>)
  2987. var hasOnlyEnd = !hasStart &&
  2988. !hasEnd &&
  2989. parts[0].indexOf('to (') === 0 &&
  2990. parts[0].charAt(parts[0].length - 1) === ')';
  2991. var startSelector = '';
  2992. var endSelector = '';
  2993. if (hasStart) {
  2994. startSelector = parts[0].slice(1, -1).trim();
  2995. }
  2996. if (hasEnd) {
  2997. endSelector = parts[1].slice(1, -1).trim();
  2998. }
  2999. if (hasOnlyEnd) {
  3000. endSelector = parts[0].slice(4, -1).trim();
  3001. }
  3002. return {
  3003. startSelector: startSelector,
  3004. endSelector: endSelector,
  3005. hasStart: hasStart,
  3006. hasEnd: hasEnd,
  3007. hasOnlyEnd: hasOnlyEnd
  3008. };
  3009. };
  3010. /**
  3011. * Checks if a selector contains pseudo-elements.
  3012. * @param {string} selector - The CSS selector to check
  3013. * @returns {boolean} True if the selector contains pseudo-elements
  3014. */
  3015. function hasPseudoElement(selector) {
  3016. // Match only double-colon (::) pseudo-elements
  3017. // Also match legacy single-colon pseudo-elements: :before, :after, :first-line, :first-letter
  3018. // These must NOT be followed by alphanumeric characters (to avoid matching :before-x or similar)
  3019. return pseudoElementRegExp.test(selector);
  3020. };
  3021. /**
  3022. * Validates balanced parentheses, brackets, and quotes in a selector.
  3023. *
  3024. * @param {string} selector - The CSS selector to validate
  3025. * @param {boolean} trackAttributes - Whether to track attribute selector context
  3026. * @param {boolean} useStack - Whether to use a stack for parentheses (needed for nested validation)
  3027. * @returns {boolean} True if the syntax is valid (all brackets, parentheses, and quotes are balanced)
  3028. */
  3029. function validateBalancedSyntax(selector, trackAttributes, useStack) {
  3030. var parenDepth = 0;
  3031. var bracketDepth = 0;
  3032. var inSingleQuote = false;
  3033. var inDoubleQuote = false;
  3034. var inAttr = false;
  3035. var stack = useStack ? [] : null;
  3036. for (var i = 0; i < selector.length; i++) {
  3037. var char = selector[i];
  3038. // Handle escape sequences - skip hex escapes or simple escapes
  3039. if (char === '\\') {
  3040. var escapeLen = getEscapeSequenceLength(selector, i);
  3041. if (escapeLen > 0) {
  3042. i += escapeLen - 1; // -1 because loop will increment
  3043. continue;
  3044. }
  3045. }
  3046. if (inSingleQuote) {
  3047. if (char === "'") {
  3048. inSingleQuote = false;
  3049. }
  3050. } else if (inDoubleQuote) {
  3051. if (char === '"') {
  3052. inDoubleQuote = false;
  3053. }
  3054. } else if (trackAttributes && inAttr) {
  3055. if (char === "]") {
  3056. inAttr = false;
  3057. } else if (char === "'") {
  3058. inSingleQuote = true;
  3059. } else if (char === '"') {
  3060. inDoubleQuote = true;
  3061. }
  3062. } else {
  3063. if (trackAttributes && char === "[") {
  3064. inAttr = true;
  3065. } else if (char === "'") {
  3066. inSingleQuote = true;
  3067. } else if (char === '"') {
  3068. inDoubleQuote = true;
  3069. } else if (char === '(') {
  3070. if (useStack) {
  3071. stack.push("(");
  3072. } else {
  3073. parenDepth++;
  3074. }
  3075. } else if (char === ')') {
  3076. if (useStack) {
  3077. if (!stack.length || stack.pop() !== "(") {
  3078. return false;
  3079. }
  3080. } else {
  3081. parenDepth--;
  3082. if (parenDepth < 0) {
  3083. return false;
  3084. }
  3085. }
  3086. } else if (char === '[') {
  3087. bracketDepth++;
  3088. } else if (char === ']') {
  3089. bracketDepth--;
  3090. if (bracketDepth < 0) {
  3091. return false;
  3092. }
  3093. }
  3094. }
  3095. }
  3096. // Check if everything is balanced
  3097. if (useStack) {
  3098. return stack.length === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote && !inAttr;
  3099. } else {
  3100. return parenDepth === 0 && bracketDepth === 0 && !inSingleQuote && !inDoubleQuote;
  3101. }
  3102. };
  3103. /**
  3104. * Checks for basic syntax errors in selectors (mismatched parentheses, brackets, quotes).
  3105. * @param {string} selector - The CSS selector to check
  3106. * @returns {boolean} True if there are syntax errors
  3107. */
  3108. function hasBasicSyntaxError(selector) {
  3109. return !validateBalancedSyntax(selector, false, false);
  3110. };
  3111. /**
  3112. * Checks for invalid combinator patterns in selectors.
  3113. * @param {string} selector - The CSS selector to check
  3114. * @returns {boolean} True if the selector contains invalid combinators
  3115. */
  3116. function hasInvalidCombinators(selector) {
  3117. // Check for invalid combinator patterns:
  3118. // - <> (not a valid combinator)
  3119. // - >> (deep descendant combinator, deprecated and invalid)
  3120. // - Multiple consecutive combinators like >>, >~, etc.
  3121. if (invalidCombinatorLtGtRegExp.test(selector)) return true;
  3122. if (invalidCombinatorDoubleGtRegExp.test(selector)) return true;
  3123. // Check for other invalid consecutive combinator patterns
  3124. if (consecutiveCombinatorsRegExp.test(selector)) return true;
  3125. return false;
  3126. };
  3127. /**
  3128. * Checks for invalid pseudo-like syntax (function calls without proper pseudo prefix).
  3129. * @param {string} selector - The CSS selector to check
  3130. * @returns {boolean} True if the selector contains invalid pseudo-like syntax
  3131. */
  3132. function hasInvalidPseudoSyntax(selector) {
  3133. // Check for specific known pseudo-elements used without : or :: prefix
  3134. // Examples: slotted(div), part(name), cue(selector)
  3135. // These are ONLY valid as ::slotted(), ::part(), ::cue()
  3136. var invalidPatterns = [
  3137. invalidSlottedRegExp,
  3138. invalidPartRegExp,
  3139. invalidCueRegExp,
  3140. invalidCueRegionRegExp
  3141. ];
  3142. for (var i = 0; i < invalidPatterns.length; i++) {
  3143. if (invalidPatterns[i].test(selector)) {
  3144. return true;
  3145. }
  3146. }
  3147. return false;
  3148. };
  3149. /**
  3150. * Checks for invalid nesting selector (&) usage.
  3151. * The & selector cannot be directly followed by a type selector without a delimiter.
  3152. * Valid: &.class, &#id, &[attr], &:hover, &::before, & div, &>div
  3153. * Invalid: &div, &span
  3154. * @param {string} selector - The CSS selector to check
  3155. * @returns {boolean} True if the selector contains invalid & usage
  3156. */
  3157. function hasInvalidNestingSelector(selector) {
  3158. // Check for & followed directly by a letter (type selector) without any delimiter
  3159. // This regex matches & followed by a letter (start of type selector) that's not preceded by an escape
  3160. // We need to exclude valid cases like &.class, &#id, &[attr], &:pseudo, &::pseudo, & (with space), &>
  3161. return invalidNestingPattern.test(selector);
  3162. };
  3163. /**
  3164. * Checks if an at-rule can be nested based on parent chain validation.
  3165. * Used for at-rules like `@counter-style`, `@property` and `@font-face` rules that can only be nested inside
  3166. * `CSSScopeRule` or `CSSConditionRule` without `CSSStyleRule` in parent chain.
  3167. * @returns {boolean} `true` if nesting is allowed, `false` otherwise
  3168. */
  3169. function canAtRuleBeNested() {
  3170. if (currentScope === topScope) {
  3171. return true; // Top-level is always allowed
  3172. }
  3173. var hasStyleRuleInChain = false;
  3174. var hasValidParent = false;
  3175. // Check currentScope
  3176. if (currentScope.constructor.name === 'CSSStyleRule') {
  3177. hasStyleRuleInChain = true;
  3178. } else if (currentScope instanceof CSSOM.CSSScopeRule || currentScope instanceof CSSOM.CSSConditionRule) {
  3179. hasValidParent = true;
  3180. }
  3181. // Check ancestorRules for CSSStyleRule
  3182. if (!hasStyleRuleInChain) {
  3183. for (var j = 0; j < ancestorRules.length; j++) {
  3184. if (ancestorRules[j].constructor.name === 'CSSStyleRule') {
  3185. hasStyleRuleInChain = true;
  3186. break;
  3187. }
  3188. if (ancestorRules[j] instanceof CSSOM.CSSScopeRule || ancestorRules[j] instanceof CSSOM.CSSConditionRule) {
  3189. hasValidParent = true;
  3190. }
  3191. }
  3192. }
  3193. // Allow nesting if we have a valid parent and no style rule in the chain
  3194. return hasValidParent && !hasStyleRuleInChain;
  3195. }
  3196. function validateAtRule(atRuleKey, validCallback, cannotBeNested) {
  3197. var isValid = false;
  3198. // Use cached regex instead of creating new one each time
  3199. var ruleRegExp = getValidationRegex(atRuleKey);
  3200. // Only slice what we need for validation (max 100 chars)
  3201. // since we only check match at position 0
  3202. var lookAheadLength = Math.min(100, token.length - i);
  3203. var ruleSlice = token.slice(i, i + lookAheadLength);
  3204. // Not all rules can be nested, if the rule cannot be nested and is in the root scope, do not perform the check
  3205. var shouldPerformCheck = cannotBeNested && currentScope !== topScope ? false : true;
  3206. // First, check if there is no invalid characters just after the at-rule
  3207. if (shouldPerformCheck && ruleSlice.search(ruleRegExp) === 0) {
  3208. // Only scan from the last known validation boundary
  3209. var searchStart = Math.max(0, lastValidationBoundary);
  3210. var beforeSlice = token.slice(searchStart, i);
  3211. // Use pre-compiled regex instead of creating new one each time
  3212. var matches = beforeSlice.match(regexBefore);
  3213. var lastI = matches ? searchStart + beforeSlice.lastIndexOf(matches[matches.length - 1]) : searchStart;
  3214. var toCheckSlice = token.slice(lastI, i);
  3215. // Check if we don't have any invalid in the portion before the `at-rule` and the closest allowed character
  3216. var checkedSlice = toCheckSlice.search(beforeRuleValidationRegExp);
  3217. if (checkedSlice === 0) {
  3218. isValid = true;
  3219. // Update the validation boundary cache to this position
  3220. lastValidationBoundary = lastI;
  3221. }
  3222. }
  3223. // Additional validation for @scope rule
  3224. if (isValid && atRuleKey === "@scope") {
  3225. var openBraceIndex = ruleSlice.indexOf('{');
  3226. if (openBraceIndex !== -1) {
  3227. // Extract the rule prelude (everything between the at-rule and {)
  3228. var rulePrelude = ruleSlice.slice(0, openBraceIndex).trim();
  3229. // Skip past at-rule keyword and whitespace
  3230. var preludeContent = rulePrelude.slice("@scope".length).trim();
  3231. if (preludeContent.length > 0) {
  3232. // Parse the scope prelude
  3233. var parsedScopePrelude = parseScopePrelude(preludeContent);
  3234. var startSelector = parsedScopePrelude.startSelector;
  3235. var endSelector = parsedScopePrelude.endSelector;
  3236. var hasStart = parsedScopePrelude.hasStart;
  3237. var hasEnd = parsedScopePrelude.hasEnd;
  3238. var hasOnlyEnd = parsedScopePrelude.hasOnlyEnd;
  3239. // Validation rules for @scope:
  3240. // 1. Empty selectors in parentheses are invalid: @scope () {} or @scope (.a) to () {}
  3241. if ((hasStart && startSelector === '') || (hasEnd && endSelector === '') || (hasOnlyEnd && endSelector === '')) {
  3242. isValid = false;
  3243. }
  3244. // 2. Pseudo-elements are invalid in scope selectors
  3245. else if ((startSelector && hasPseudoElement(startSelector)) || (endSelector && hasPseudoElement(endSelector))) {
  3246. isValid = false;
  3247. }
  3248. // 3. Basic syntax errors (mismatched parens, brackets, quotes)
  3249. else if ((startSelector && hasBasicSyntaxError(startSelector)) || (endSelector && hasBasicSyntaxError(endSelector))) {
  3250. isValid = false;
  3251. }
  3252. // 4. Invalid combinator patterns
  3253. else if ((startSelector && hasInvalidCombinators(startSelector)) || (endSelector && hasInvalidCombinators(endSelector))) {
  3254. isValid = false;
  3255. }
  3256. // 5. Invalid pseudo-like syntax (function without : or :: prefix)
  3257. else if ((startSelector && hasInvalidPseudoSyntax(startSelector)) || (endSelector && hasInvalidPseudoSyntax(endSelector))) {
  3258. isValid = false;
  3259. }
  3260. // 6. Invalid structure (no proper parentheses found when prelude is not empty)
  3261. else if (!hasStart && !hasOnlyEnd) {
  3262. isValid = false;
  3263. }
  3264. }
  3265. // Empty prelude (@scope {}) is valid
  3266. }
  3267. }
  3268. if (isValid && atRuleKey === "@page") {
  3269. var openBraceIndex = ruleSlice.indexOf('{');
  3270. if (openBraceIndex !== -1) {
  3271. // Extract the rule prelude (everything between the at-rule and {)
  3272. var rulePrelude = ruleSlice.slice(0, openBraceIndex).trim();
  3273. // Skip past at-rule keyword and whitespace
  3274. var preludeContent = rulePrelude.slice("@page".length).trim();
  3275. if (preludeContent.length > 0) {
  3276. var trimmedValue = preludeContent.trim();
  3277. // Empty selector is valid for @page
  3278. if (trimmedValue !== '') {
  3279. // Parse @page selectorText for page name and pseudo-pages
  3280. // Valid formats:
  3281. // - (empty - no name, no pseudo-page)
  3282. // - :left, :right, :first, :blank (pseudo-page only)
  3283. // - named (named page only)
  3284. // - named:first (named page with single pseudo-page)
  3285. // - named:first:left (named page with multiple pseudo-pages)
  3286. var match = trimmedValue.match(atPageRuleSelectorRegExp);
  3287. if (match) {
  3288. var pageName = match[1] || '';
  3289. var pseudoPages = match[2] || '';
  3290. // Validate page name if present
  3291. if (pageName) {
  3292. if (!cssCustomIdentifierRegExp.test(pageName)) {
  3293. isValid = false;
  3294. }
  3295. }
  3296. // Validate pseudo-pages if present
  3297. if (pseudoPages) {
  3298. var pseudos = pseudoPages.split(':').filter(function (p) { return p; });
  3299. var validPseudos = ['left', 'right', 'first', 'blank'];
  3300. var allValid = true;
  3301. for (var j = 0; j < pseudos.length; j++) {
  3302. if (validPseudos.indexOf(pseudos[j].toLowerCase()) === -1) {
  3303. allValid = false;
  3304. break;
  3305. }
  3306. }
  3307. if (!allValid) {
  3308. isValid = false;
  3309. }
  3310. }
  3311. } else {
  3312. isValid = false;
  3313. }
  3314. }
  3315. }
  3316. }
  3317. }
  3318. if (!isValid) {
  3319. // If it's invalid the browser will simply ignore the entire invalid block
  3320. // Use regex to find the closing brace of the invalid rule
  3321. // Regex used above is not ES5 compliant. Using alternative.
  3322. // var ruleStatementMatch = ruleSlice.match(atRulesStatemenRegExp); //
  3323. var ruleStatementMatch = atRulesStatemenRegExpES5Alternative(ruleSlice);
  3324. // If it's a statement inside a nested rule, ignore only the statement
  3325. if (ruleStatementMatch && currentScope !== topScope) {
  3326. var ignoreEnd = ruleStatementMatch[0].indexOf(";");
  3327. i += ruleStatementMatch.index + ignoreEnd;
  3328. return;
  3329. }
  3330. // Check if there's a semicolon before the invalid at-rule and the first opening brace
  3331. if (atRuleKey === "@layer") {
  3332. var ruleSemicolonAndOpeningBraceMatch = ruleSlice.match(forwardRuleSemicolonAndOpeningBraceRegExp);
  3333. if (ruleSemicolonAndOpeningBraceMatch && ruleSemicolonAndOpeningBraceMatch[1] === ";") {
  3334. // Ignore the rule block until the semicolon
  3335. i += ruleSemicolonAndOpeningBraceMatch.index + ruleSemicolonAndOpeningBraceMatch[0].length;
  3336. state = "before-selector";
  3337. return;
  3338. }
  3339. }
  3340. // Ignore the entire rule block (if it's a statement it should ignore the statement plus the next block)
  3341. i = ignoreBalancedBlock(i, ruleSlice);
  3342. state = "before-selector";
  3343. } else {
  3344. validCallback.call(this);
  3345. }
  3346. }
  3347. // Helper functions for looseSelectorValidator
  3348. // Defined outside to avoid recreation on every validation call
  3349. /**
  3350. * Check if character is a valid identifier start
  3351. * @param {string} c - Character to check
  3352. * @returns {boolean}
  3353. */
  3354. function isIdentStart(c) {
  3355. return /[a-zA-Z_\u00A0-\uFFFF]/.test(c);
  3356. }
  3357. /**
  3358. * Check if character is a valid identifier character
  3359. * @param {string} c - Character to check
  3360. * @returns {boolean}
  3361. */
  3362. function isIdentChar(c) {
  3363. return /[a-zA-Z0-9_\u00A0-\uFFFF\-]/.test(c);
  3364. }
  3365. /**
  3366. * Helper function to validate CSS selector syntax without regex backtracking.
  3367. * Iteratively parses the selector string to identify valid components.
  3368. *
  3369. * Supports:
  3370. * - Escaped characters (e.g., .class\!, #id\@name)
  3371. * - Namespace selectors (ns|element, *|element, |element)
  3372. * - All standard CSS selectors (class, ID, type, attribute, pseudo, etc.)
  3373. * - Combinators (>, +, ~, whitespace)
  3374. * - Nesting selector (&)
  3375. *
  3376. * This approach eliminates exponential backtracking by using explicit character-by-character
  3377. * parsing instead of nested quantifiers in regex.
  3378. *
  3379. * @param {string} selector - The selector to validate
  3380. * @returns {boolean} - True if valid selector syntax
  3381. */
  3382. function looseSelectorValidator(selector) {
  3383. if (!selector || selector.length === 0) {
  3384. return false;
  3385. }
  3386. var i = 0;
  3387. var len = selector.length;
  3388. var hasMatchedComponent = false;
  3389. // Helper: Skip escaped character (backslash + hex escape or any char)
  3390. function skipEscape() {
  3391. if (i < len && selector[i] === '\\') {
  3392. var escapeLen = getEscapeSequenceLength(selector, i);
  3393. if (escapeLen > 0) {
  3394. i += escapeLen; // Skip entire escape sequence
  3395. return true;
  3396. }
  3397. }
  3398. return false;
  3399. }
  3400. // Helper: Parse identifier (with possible escapes)
  3401. function parseIdentifier() {
  3402. var start = i;
  3403. while (i < len) {
  3404. if (skipEscape()) {
  3405. continue;
  3406. } else if (isIdentChar(selector[i])) {
  3407. i++;
  3408. } else {
  3409. break;
  3410. }
  3411. }
  3412. return i > start;
  3413. }
  3414. // Helper: Parse namespace prefix (optional)
  3415. function parseNamespace() {
  3416. var start = i;
  3417. // Match: *| or identifier| or |
  3418. if (i < len && selector[i] === '*') {
  3419. i++;
  3420. } else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\')) {
  3421. parseIdentifier();
  3422. }
  3423. if (i < len && selector[i] === '|') {
  3424. i++;
  3425. return true;
  3426. }
  3427. // Rollback if no pipe found
  3428. i = start;
  3429. return false;
  3430. }
  3431. // Helper: Parse pseudo-class/element arguments (with balanced parens)
  3432. function parsePseudoArgs() {
  3433. if (i >= len || selector[i] !== '(') {
  3434. return false;
  3435. }
  3436. i++; // Skip opening paren
  3437. var depth = 1;
  3438. var inString = false;
  3439. var stringChar = '';
  3440. while (i < len && depth > 0) {
  3441. var c = selector[i];
  3442. if (c === '\\' && i + 1 < len) {
  3443. i += 2; // Skip escaped character
  3444. } else if (!inString && (c === '"' || c === '\'')) {
  3445. inString = true;
  3446. stringChar = c;
  3447. i++;
  3448. } else if (inString && c === stringChar) {
  3449. inString = false;
  3450. i++;
  3451. } else if (!inString && c === '(') {
  3452. depth++;
  3453. i++;
  3454. } else if (!inString && c === ')') {
  3455. depth--;
  3456. i++;
  3457. } else {
  3458. i++;
  3459. }
  3460. }
  3461. return depth === 0;
  3462. }
  3463. // Main parsing loop
  3464. while (i < len) {
  3465. var matched = false;
  3466. var start = i;
  3467. // Skip whitespace
  3468. while (i < len && /\s/.test(selector[i])) {
  3469. i++;
  3470. }
  3471. if (i > start) {
  3472. hasMatchedComponent = true;
  3473. continue;
  3474. }
  3475. // Match combinators: >, +, ~
  3476. if (i < len && /[>+~]/.test(selector[i])) {
  3477. i++;
  3478. hasMatchedComponent = true;
  3479. // Skip trailing whitespace
  3480. while (i < len && /\s/.test(selector[i])) {
  3481. i++;
  3482. }
  3483. continue;
  3484. }
  3485. // Match nesting selector: &
  3486. if (i < len && selector[i] === '&') {
  3487. i++;
  3488. hasMatchedComponent = true;
  3489. matched = true;
  3490. }
  3491. // Match class selector: .identifier
  3492. else if (i < len && selector[i] === '.') {
  3493. i++;
  3494. if (parseIdentifier()) {
  3495. hasMatchedComponent = true;
  3496. matched = true;
  3497. }
  3498. }
  3499. // Match ID selector: #identifier
  3500. else if (i < len && selector[i] === '#') {
  3501. i++;
  3502. if (parseIdentifier()) {
  3503. hasMatchedComponent = true;
  3504. matched = true;
  3505. }
  3506. }
  3507. // Match pseudo-class/element: :identifier or ::identifier
  3508. else if (i < len && selector[i] === ':') {
  3509. i++;
  3510. if (i < len && selector[i] === ':') {
  3511. i++; // Pseudo-element
  3512. }
  3513. if (parseIdentifier()) {
  3514. parsePseudoArgs(); // Optional arguments
  3515. hasMatchedComponent = true;
  3516. matched = true;
  3517. }
  3518. }
  3519. // Match attribute selector: [...]
  3520. else if (i < len && selector[i] === '[') {
  3521. i++;
  3522. var depth = 1;
  3523. while (i < len && depth > 0) {
  3524. if (selector[i] === '\\') {
  3525. i += 2;
  3526. } else if (selector[i] === '\'') {
  3527. i++;
  3528. while (i < len && selector[i] !== '\'') {
  3529. if (selector[i] === '\\') i += 2;
  3530. else i++;
  3531. }
  3532. if (i < len) i++; // Skip closing quote
  3533. } else if (selector[i] === '"') {
  3534. i++;
  3535. while (i < len && selector[i] !== '"') {
  3536. if (selector[i] === '\\') i += 2;
  3537. else i++;
  3538. }
  3539. if (i < len) i++; // Skip closing quote
  3540. } else if (selector[i] === '[') {
  3541. depth++;
  3542. i++;
  3543. } else if (selector[i] === ']') {
  3544. depth--;
  3545. i++;
  3546. } else {
  3547. i++;
  3548. }
  3549. }
  3550. if (depth === 0) {
  3551. hasMatchedComponent = true;
  3552. matched = true;
  3553. }
  3554. }
  3555. // Match type selector with optional namespace: [namespace|]identifier
  3556. else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\' || selector[i] === '*' || selector[i] === '|')) {
  3557. parseNamespace(); // Optional namespace prefix
  3558. if (i < len && selector[i] === '*') {
  3559. i++; // Universal selector
  3560. hasMatchedComponent = true;
  3561. matched = true;
  3562. } else if (i < len && (isIdentStart(selector[i]) || selector[i] === '\\')) {
  3563. if (parseIdentifier()) {
  3564. hasMatchedComponent = true;
  3565. matched = true;
  3566. }
  3567. }
  3568. }
  3569. // If no match found, invalid selector
  3570. if (!matched && i === start) {
  3571. return false;
  3572. }
  3573. }
  3574. return hasMatchedComponent;
  3575. }
  3576. /**
  3577. * Validates a basic CSS selector, allowing for deeply nested balanced parentheses in pseudo-classes.
  3578. * This function replaces the previous basicSelectorRegExp.
  3579. *
  3580. * This function matches:
  3581. * - Type selectors (e.g., `div`, `span`)
  3582. * - Universal selector (`*`)
  3583. * - Namespace selectors (e.g., `*|div`, `custom|div`, `|div`)
  3584. * - ID selectors (e.g., `#header`, `#a\ b`, `#åèiöú`)
  3585. * - Class selectors (e.g., `.container`, `.a\ b`, `.åèiöú`)
  3586. * - Attribute selectors (e.g., `[type="text"]`)
  3587. * - Pseudo-classes and pseudo-elements (e.g., `:hover`, `::before`, `:nth-child(2)`)
  3588. * - Pseudo-classes with nested parentheses, including cases where parentheses are nested inside arguments,
  3589. * such as `:has(.sel:nth-child(3n))`
  3590. * - The parent selector (`&`)
  3591. * - Combinators (`>`, `+`, `~`) with optional whitespace
  3592. * - Whitespace (descendant combinator)
  3593. *
  3594. * Unicode and escape sequences are allowed in identifiers.
  3595. *
  3596. * @param {string} selector
  3597. * @returns {boolean}
  3598. */
  3599. function basicSelectorValidator(selector) {
  3600. // Guard against extremely long selectors to prevent potential regex performance issues
  3601. // Reasonable selectors are typically under 1000 characters
  3602. if (selector.length > 10000) {
  3603. return false;
  3604. }
  3605. // Validate balanced syntax with attribute tracking and stack-based parentheses matching
  3606. if (!validateBalancedSyntax(selector, true, true)) {
  3607. return false;
  3608. }
  3609. // Check for invalid combinator patterns
  3610. if (hasInvalidCombinators(selector)) {
  3611. return false;
  3612. }
  3613. // Check for invalid pseudo-like syntax
  3614. if (hasInvalidPseudoSyntax(selector)) {
  3615. return false;
  3616. }
  3617. // Check for invalid nesting selector (&) usage
  3618. if (hasInvalidNestingSelector(selector)) {
  3619. return false;
  3620. }
  3621. // Check for invalid pseudo-class usage with quoted strings
  3622. // Pseudo-classes like :lang(), :dir(), :nth-*() should not accept quoted strings
  3623. // Using iterative parsing instead of regex to avoid exponential backtracking
  3624. var noQuotesPseudos = ['lang', 'dir', 'nth-child', 'nth-last-child', 'nth-of-type', 'nth-last-of-type'];
  3625. for (var idx = 0; idx < selector.length; idx++) {
  3626. // Look for pseudo-class/element start
  3627. if (selector[idx] === ':') {
  3628. var pseudoStart = idx;
  3629. idx++;
  3630. // Skip second colon for pseudo-elements
  3631. if (idx < selector.length && selector[idx] === ':') {
  3632. idx++;
  3633. }
  3634. // Extract pseudo name
  3635. var nameStart = idx;
  3636. while (idx < selector.length && /[a-zA-Z0-9\-]/.test(selector[idx])) {
  3637. idx++;
  3638. }
  3639. if (idx === nameStart) {
  3640. continue; // No name found
  3641. }
  3642. var pseudoName = selector.substring(nameStart, idx).toLowerCase();
  3643. // Check if this pseudo has arguments
  3644. if (idx < selector.length && selector[idx] === '(') {
  3645. idx++;
  3646. var contentStart = idx;
  3647. var depth = 1;
  3648. // Find matching closing paren (handle nesting)
  3649. while (idx < selector.length && depth > 0) {
  3650. if (selector[idx] === '\\') {
  3651. idx += 2; // Skip escaped character
  3652. } else if (selector[idx] === '(') {
  3653. depth++;
  3654. idx++;
  3655. } else if (selector[idx] === ')') {
  3656. depth--;
  3657. idx++;
  3658. } else {
  3659. idx++;
  3660. }
  3661. }
  3662. if (depth === 0) {
  3663. var pseudoContent = selector.substring(contentStart, idx - 1);
  3664. // Check if this pseudo should not have quoted strings
  3665. for (var j = 0; j < noQuotesPseudos.length; j++) {
  3666. if (pseudoName === noQuotesPseudos[j] && /['"]/.test(pseudoContent)) {
  3667. return false;
  3668. }
  3669. }
  3670. }
  3671. }
  3672. }
  3673. }
  3674. // Use the iterative validator to avoid regex backtracking issues
  3675. return looseSelectorValidator(selector);
  3676. }
  3677. /**
  3678. * Regular expression to match CSS pseudo-classes with arguments.
  3679. *
  3680. * Matches patterns like `:pseudo-class(argument)`, capturing the pseudo-class name and its argument.
  3681. *
  3682. * Capture groups:
  3683. * 1. The pseudo-class name (letters and hyphens).
  3684. * 2. The argument inside the parentheses (can contain nested parentheses, quoted strings, and other characters.).
  3685. *
  3686. * Global flag (`g`) is used to find all matches in the input string.
  3687. *
  3688. * Example matches:
  3689. * - :nth-child(2n+1)
  3690. * - :has(.sel:nth-child(3n))
  3691. * - :not(".foo, .bar")
  3692. *
  3693. * REPLACED WITH FUNCTION to avoid exponential backtracking.
  3694. */
  3695. /**
  3696. * Extract pseudo-classes with arguments from a selector using iterative parsing.
  3697. * Replaces the previous globalPseudoClassRegExp to avoid exponential backtracking.
  3698. *
  3699. * Handles:
  3700. * - Regular content without parentheses or quotes
  3701. * - Single-quoted strings
  3702. * - Double-quoted strings
  3703. * - Nested parentheses (arbitrary depth)
  3704. *
  3705. * @param {string} selector - The CSS selector to parse
  3706. * @returns {Array} Array of matches, each with: [fullMatch, pseudoName, pseudoArgs, startIndex]
  3707. */
  3708. function extractPseudoClasses(selector) {
  3709. var matches = [];
  3710. for (var i = 0; i < selector.length; i++) {
  3711. // Look for pseudo-class start (single or double colon)
  3712. if (selector[i] === ':') {
  3713. var pseudoStart = i;
  3714. i++;
  3715. // Skip second colon for pseudo-elements (::)
  3716. if (i < selector.length && selector[i] === ':') {
  3717. i++;
  3718. }
  3719. // Extract pseudo name
  3720. var nameStart = i;
  3721. while (i < selector.length && /[a-zA-Z\-]/.test(selector[i])) {
  3722. i++;
  3723. }
  3724. if (i === nameStart) {
  3725. continue; // No name found
  3726. }
  3727. var pseudoName = selector.substring(nameStart, i);
  3728. // Check if this pseudo has arguments
  3729. if (i < selector.length && selector[i] === '(') {
  3730. i++;
  3731. var argsStart = i;
  3732. var depth = 1;
  3733. var inSingleQuote = false;
  3734. var inDoubleQuote = false;
  3735. // Find matching closing paren (handle nesting and strings)
  3736. while (i < selector.length && depth > 0) {
  3737. var ch = selector[i];
  3738. if (ch === '\\') {
  3739. i += 2; // Skip escaped character
  3740. } else if (ch === "'" && !inDoubleQuote) {
  3741. inSingleQuote = !inSingleQuote;
  3742. i++;
  3743. } else if (ch === '"' && !inSingleQuote) {
  3744. inDoubleQuote = !inDoubleQuote;
  3745. i++;
  3746. } else if (ch === '(' && !inSingleQuote && !inDoubleQuote) {
  3747. depth++;
  3748. i++;
  3749. } else if (ch === ')' && !inSingleQuote && !inDoubleQuote) {
  3750. depth--;
  3751. i++;
  3752. } else {
  3753. i++;
  3754. }
  3755. }
  3756. if (depth === 0) {
  3757. var pseudoArgs = selector.substring(argsStart, i - 1);
  3758. var fullMatch = selector.substring(pseudoStart, i);
  3759. // Store match in same format as regex: [fullMatch, pseudoName, pseudoArgs, startIndex]
  3760. matches.push([fullMatch, pseudoName, pseudoArgs, pseudoStart]);
  3761. }
  3762. // Move back one since loop will increment
  3763. i--;
  3764. }
  3765. }
  3766. }
  3767. return matches;
  3768. }
  3769. /**
  3770. * Parses a CSS selector string and splits it into parts, handling nested parentheses.
  3771. *
  3772. * This function is useful for splitting selectors that may contain nested function-like
  3773. * syntax (e.g., :not(.foo, .bar)), ensuring that commas inside parentheses do not split
  3774. * the selector.
  3775. *
  3776. * @param {string} selector - The CSS selector string to parse.
  3777. * @returns {string[]} An array of selector parts, split by top-level commas, with whitespace trimmed.
  3778. */
  3779. function parseAndSplitNestedSelectors(selector) {
  3780. var depth = 0; // Track parenthesis nesting depth
  3781. var buffer = ""; // Accumulate characters for current selector part
  3782. var parts = []; // Array of split selector parts
  3783. var inSingleQuote = false; // Track if we're inside single quotes
  3784. var inDoubleQuote = false; // Track if we're inside double quotes
  3785. var i, char;
  3786. for (i = 0; i < selector.length; i++) {
  3787. char = selector.charAt(i);
  3788. // Handle escape sequences - skip them entirely
  3789. if (char === '\\' && i + 1 < selector.length) {
  3790. buffer += char;
  3791. i++;
  3792. buffer += selector.charAt(i);
  3793. continue;
  3794. }
  3795. // Handle single quote strings
  3796. if (char === "'" && !inDoubleQuote) {
  3797. inSingleQuote = !inSingleQuote;
  3798. buffer += char;
  3799. }
  3800. // Handle double quote strings
  3801. else if (char === '"' && !inSingleQuote) {
  3802. inDoubleQuote = !inDoubleQuote;
  3803. buffer += char;
  3804. }
  3805. // Process characters outside of quoted strings
  3806. else if (!inSingleQuote && !inDoubleQuote) {
  3807. if (char === '(') {
  3808. // Entering a nested level (e.g., :is(...))
  3809. depth++;
  3810. buffer += char;
  3811. } else if (char === ')') {
  3812. // Exiting a nested level
  3813. depth--;
  3814. buffer += char;
  3815. } else if (char === ',' && depth === 0) {
  3816. // Found a top-level comma separator - split here
  3817. // Note: escaped commas (\,) are already handled above
  3818. if (buffer.trim()) {
  3819. parts.push(buffer.trim());
  3820. }
  3821. buffer = "";
  3822. } else {
  3823. // Regular character - add to buffer
  3824. buffer += char;
  3825. }
  3826. }
  3827. // Characters inside quoted strings - add to buffer
  3828. else {
  3829. buffer += char;
  3830. }
  3831. }
  3832. // Add any remaining content in buffer as the last part
  3833. var trimmed = buffer.trim();
  3834. if (trimmed) {
  3835. // Preserve trailing space if selector ends with hex escape
  3836. var endsWithHexEscape = endsWithHexEscapeRegExp.test(buffer);
  3837. parts.push(endsWithHexEscape ? buffer.replace(leadingWhitespaceRegExp, '') : trimmed);
  3838. }
  3839. return parts;
  3840. }
  3841. /**
  3842. * Validates a CSS selector string, including handling of nested selectors within certain pseudo-classes.
  3843. *
  3844. * This function checks if the provided selector is valid according to the rules defined by
  3845. * `basicSelectorValidator`. For pseudo-classes that accept selector lists (such as :not, :is, :has, :where),
  3846. * it recursively validates each nested selector using the same validation logic.
  3847. *
  3848. * @param {string} selector - The CSS selector string to validate.
  3849. * @returns {boolean} Returns `true` if the selector is valid, otherwise `false`.
  3850. */
  3851. // Cache to store validated selectors (previously a ES6 Map, now an ES5-compliant object)
  3852. var validatedSelectorsCache = {};
  3853. // Only pseudo-classes that accept selector lists should recurse
  3854. var selectorListPseudoClasses = {
  3855. 'not': true,
  3856. 'is': true,
  3857. 'has': true,
  3858. 'where': true
  3859. };
  3860. function validateSelector(selector) {
  3861. if (validatedSelectorsCache.hasOwnProperty(selector)) {
  3862. return validatedSelectorsCache[selector];
  3863. }
  3864. // Use function-based parsing to extract pseudo-classes (avoids backtracking)
  3865. var pseudoClassMatches = extractPseudoClasses(selector);
  3866. for (var j = 0; j < pseudoClassMatches.length; j++) {
  3867. var pseudoClass = pseudoClassMatches[j][1];
  3868. if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) {
  3869. var nestedSelectors = parseAndSplitNestedSelectors(pseudoClassMatches[j][2]);
  3870. // Check if ANY selector in the list contains & (nesting selector)
  3871. // If so, skip validation for the entire selector list since & will be replaced at runtime
  3872. var hasAmpersand = false;
  3873. for (var k = 0; k < nestedSelectors.length; k++) {
  3874. if (ampersandRegExp.test(nestedSelectors[k])) {
  3875. hasAmpersand = true;
  3876. break;
  3877. }
  3878. }
  3879. // If any selector has &, skip validation for this entire pseudo-class
  3880. if (hasAmpersand) {
  3881. continue;
  3882. }
  3883. // Otherwise, validate each selector normally
  3884. for (var i = 0; i < nestedSelectors.length; i++) {
  3885. var nestedSelector = nestedSelectors[i];
  3886. if (!validatedSelectorsCache.hasOwnProperty(nestedSelector)) {
  3887. var nestedSelectorValidation = validateSelector(nestedSelector);
  3888. validatedSelectorsCache[nestedSelector] = nestedSelectorValidation;
  3889. if (!nestedSelectorValidation) {
  3890. validatedSelectorsCache[selector] = false;
  3891. return false;
  3892. }
  3893. } else if (!validatedSelectorsCache[nestedSelector]) {
  3894. validatedSelectorsCache[selector] = false;
  3895. return false;
  3896. }
  3897. }
  3898. }
  3899. }
  3900. var basicSelectorValidation = basicSelectorValidator(selector);
  3901. validatedSelectorsCache[selector] = basicSelectorValidation;
  3902. return basicSelectorValidation;
  3903. }
  3904. /**
  3905. * Validates namespace selectors by checking if the namespace prefix is defined.
  3906. *
  3907. * @param {string} selector - The CSS selector to validate
  3908. * @returns {boolean} Returns true if the namespace is valid, false otherwise
  3909. */
  3910. function validateNamespaceSelector(selector) {
  3911. // Check if selector contains a namespace prefix
  3912. // We need to ignore pipes inside attribute selectors
  3913. var pipeIndex = -1;
  3914. var inAttr = false;
  3915. var inSingleQuote = false;
  3916. var inDoubleQuote = false;
  3917. for (var i = 0; i < selector.length; i++) {
  3918. var char = selector[i];
  3919. // Handle escape sequences - skip hex escapes or simple escapes
  3920. if (char === '\\') {
  3921. var escapeLen = getEscapeSequenceLength(selector, i);
  3922. if (escapeLen > 0) {
  3923. i += escapeLen - 1; // -1 because loop will increment
  3924. continue;
  3925. }
  3926. }
  3927. if (inSingleQuote) {
  3928. if (char === "'") {
  3929. inSingleQuote = false;
  3930. }
  3931. } else if (inDoubleQuote) {
  3932. if (char === '"') {
  3933. inDoubleQuote = false;
  3934. }
  3935. } else if (inAttr) {
  3936. if (char === "]") {
  3937. inAttr = false;
  3938. } else if (char === "'") {
  3939. inSingleQuote = true;
  3940. } else if (char === '"') {
  3941. inDoubleQuote = true;
  3942. }
  3943. } else {
  3944. if (char === "[") {
  3945. inAttr = true;
  3946. } else if (char === "|" && !inAttr) {
  3947. // This is a namespace separator, not an attribute operator
  3948. pipeIndex = i;
  3949. break;
  3950. }
  3951. }
  3952. }
  3953. if (pipeIndex === -1) {
  3954. return true; // No namespace, always valid
  3955. }
  3956. var namespacePrefix = selector.substring(0, pipeIndex);
  3957. // Universal namespace (*|) and default namespace (|) are always valid
  3958. if (namespacePrefix === '*' || namespacePrefix === '') {
  3959. return true;
  3960. }
  3961. // Check if the custom namespace prefix is defined
  3962. return definedNamespacePrefixes.hasOwnProperty(namespacePrefix);
  3963. }
  3964. /**
  3965. * Normalizes escape sequences in a selector to match browser behavior.
  3966. * Decodes escape sequences and re-encodes them in canonical form.
  3967. *
  3968. * @param {string} selector - The selector to normalize
  3969. * @returns {string} Normalized selector
  3970. */
  3971. function normalizeSelectorEscapes(selector) {
  3972. var result = '';
  3973. var i = 0;
  3974. var nextChar = '';
  3975. // Track context for identifier boundaries
  3976. var inIdentifier = false;
  3977. var inAttribute = false;
  3978. var attributeDepth = 0;
  3979. var needsEscapeForIdent = false;
  3980. var lastWasHexEscape = false;
  3981. while (i < selector.length) {
  3982. var char = selector[i];
  3983. // Track attribute selector context
  3984. if (char === '[' && !inAttribute) {
  3985. inAttribute = true;
  3986. attributeDepth = 1;
  3987. result += char;
  3988. i++;
  3989. needsEscapeForIdent = false;
  3990. inIdentifier = false;
  3991. lastWasHexEscape = false;
  3992. continue;
  3993. }
  3994. if (inAttribute) {
  3995. if (char === '[') attributeDepth++;
  3996. if (char === ']') {
  3997. attributeDepth--;
  3998. if (attributeDepth === 0) inAttribute = false;
  3999. }
  4000. // Don't normalize escapes inside attribute selectors
  4001. if (char === '\\' && i + 1 < selector.length) {
  4002. var escapeLen = getEscapeSequenceLength(selector, i);
  4003. result += selector.substr(i, escapeLen);
  4004. i += escapeLen;
  4005. } else {
  4006. result += char;
  4007. i++;
  4008. }
  4009. lastWasHexEscape = false;
  4010. continue;
  4011. }
  4012. // Handle escape sequences
  4013. if (char === '\\') {
  4014. var escapeLen = getEscapeSequenceLength(selector, i);
  4015. if (escapeLen > 0) {
  4016. var escapeSeq = selector.substr(i, escapeLen);
  4017. var decoded = decodeEscapeSequence(escapeSeq);
  4018. var wasHexEscape = startsWithHexEscapeRegExp.test(escapeSeq);
  4019. var hadTerminatingSpace = wasHexEscape && escapeSeq[escapeLen - 1] === ' ';
  4020. nextChar = selector[i + escapeLen] || '';
  4021. // Check if this character needs escaping
  4022. var needsEscape = false;
  4023. var useHexEscape = false;
  4024. if (needsEscapeForIdent) {
  4025. // At start of identifier (after . # or -)
  4026. // Digits must be escaped, letters/underscore/_/- don't need escaping
  4027. if (isDigit(decoded)) {
  4028. needsEscape = true;
  4029. useHexEscape = true;
  4030. } else if (decoded === '-') {
  4031. // Dash at identifier start: keep escaped if it's the only character,
  4032. // otherwise it can be decoded
  4033. var remainingSelector = selector.substring(i + escapeLen);
  4034. var hasMoreIdentChars = remainingSelector && identCharRegExp.test(remainingSelector[0]);
  4035. needsEscape = !hasMoreIdentChars;
  4036. } else if (!identStartCharRegExp.test(decoded)) {
  4037. needsEscape = true;
  4038. }
  4039. } else {
  4040. if (specialCharsNeedEscapeRegExp.test(decoded)) {
  4041. needsEscape = true;
  4042. }
  4043. }
  4044. if (needsEscape) {
  4045. if (useHexEscape) {
  4046. // Use normalized hex escape
  4047. var codePoint = decoded.charCodeAt(0);
  4048. var hex = codePoint.toString(16);
  4049. result += '\\' + hex;
  4050. // Add space if next char could continue the hex sequence,
  4051. // or if at end of selector (to disambiguate the escape)
  4052. if (isHexDigit(nextChar) || !nextChar || afterHexEscapeSeparatorRegExp.test(nextChar)) {
  4053. result += ' ';
  4054. lastWasHexEscape = false;
  4055. } else {
  4056. lastWasHexEscape = true;
  4057. }
  4058. } else {
  4059. // Use simple character escape
  4060. result += '\\' + decoded;
  4061. lastWasHexEscape = false;
  4062. }
  4063. } else {
  4064. // No escape needed, use the character directly
  4065. // But if previous was hex escape (without terminating space) and this is alphanumeric, add space
  4066. if (lastWasHexEscape && !hadTerminatingSpace && isAlphanumeric(decoded)) {
  4067. result += ' ';
  4068. }
  4069. result += decoded;
  4070. // Preserve terminating space at end of selector (when followed by non-ident char)
  4071. if (hadTerminatingSpace && (!nextChar || afterHexEscapeSeparatorRegExp.test(nextChar))) {
  4072. result += ' ';
  4073. }
  4074. lastWasHexEscape = false;
  4075. }
  4076. i += escapeLen;
  4077. // After processing escape, check if we're still needing ident validation
  4078. // Only stay in needsEscapeForIdent state if decoded was '-'
  4079. needsEscapeForIdent = needsEscapeForIdent && decoded === '-';
  4080. inIdentifier = true;
  4081. continue;
  4082. }
  4083. }
  4084. // Handle regular characters
  4085. if (char === '.' || char === '#') {
  4086. result += char;
  4087. needsEscapeForIdent = true;
  4088. inIdentifier = false;
  4089. lastWasHexEscape = false;
  4090. i++;
  4091. } else if (char === '-' && needsEscapeForIdent) {
  4092. // Dash after . or # - next char must be valid ident start or digit (which needs escaping)
  4093. result += char;
  4094. needsEscapeForIdent = true;
  4095. lastWasHexEscape = false;
  4096. i++;
  4097. } else if (isDigit(char) && needsEscapeForIdent) {
  4098. // Digit at identifier start must be hex escaped
  4099. var codePoint = char.charCodeAt(0);
  4100. var hex = codePoint.toString(16);
  4101. result += '\\' + hex;
  4102. nextChar = selector[i + 1] || '';
  4103. // Add space if next char could continue the hex sequence,
  4104. // or if at end of selector (to disambiguate the escape)
  4105. if (isHexDigit(nextChar) || !nextChar || afterHexEscapeSeparatorRegExp.test(nextChar)) {
  4106. result += ' ';
  4107. lastWasHexEscape = false;
  4108. } else {
  4109. lastWasHexEscape = true;
  4110. }
  4111. needsEscapeForIdent = false;
  4112. inIdentifier = true;
  4113. i++;
  4114. } else if (char === ':' || combinatorOrSeparatorRegExp.test(char)) {
  4115. // Combinators, separators, and pseudo-class markers reset identifier state
  4116. // Preserve trailing space from hex escape
  4117. if (!(char === ' ' && lastWasHexEscape && result[result.length - 1] === ' ')) {
  4118. result += char;
  4119. }
  4120. needsEscapeForIdent = false;
  4121. inIdentifier = false;
  4122. lastWasHexEscape = false;
  4123. i++;
  4124. } else if (isLetter(char) && lastWasHexEscape) {
  4125. // Letter after hex escape needs a space separator
  4126. result += ' ' + char;
  4127. needsEscapeForIdent = false;
  4128. inIdentifier = true;
  4129. lastWasHexEscape = false;
  4130. i++;
  4131. } else if (char === ' ' && lastWasHexEscape) {
  4132. // Trailing space - keep it if at end or before non-ident char
  4133. nextChar = selector[i + 1] || '';
  4134. if (!nextChar || trailingSpaceSeparatorRegExp.test(nextChar)) {
  4135. result += char;
  4136. }
  4137. needsEscapeForIdent = false;
  4138. inIdentifier = false;
  4139. lastWasHexEscape = false;
  4140. i++;
  4141. } else {
  4142. result += char;
  4143. needsEscapeForIdent = false;
  4144. inIdentifier = true;
  4145. lastWasHexEscape = false;
  4146. i++;
  4147. }
  4148. }
  4149. return result;
  4150. }
  4151. /**
  4152. * Helper function to decode all escape sequences in a string.
  4153. *
  4154. * @param {string} str - The string to decode
  4155. * @returns {string} The decoded string
  4156. */
  4157. function decodeEscapeSequencesInString(str) {
  4158. var result = '';
  4159. for (var i = 0; i < str.length; i++) {
  4160. if (str[i] === '\\' && i + 1 < str.length) {
  4161. // Get the escape sequence length
  4162. var escapeLen = getEscapeSequenceLength(str, i);
  4163. if (escapeLen > 0) {
  4164. var escapeSeq = str.substr(i, escapeLen);
  4165. var decoded = decodeEscapeSequence(escapeSeq);
  4166. result += decoded;
  4167. i += escapeLen - 1; // -1 because loop will increment
  4168. continue;
  4169. }
  4170. }
  4171. result += str[i];
  4172. }
  4173. return result;
  4174. }
  4175. /**
  4176. * Decodes a CSS escape sequence to its character value.
  4177. *
  4178. * @param {string} escapeSeq - The escape sequence (including backslash)
  4179. * @returns {string} The decoded character
  4180. */
  4181. function decodeEscapeSequence(escapeSeq) {
  4182. if (escapeSeq.length < 2 || escapeSeq[0] !== '\\') {
  4183. return escapeSeq;
  4184. }
  4185. var content = escapeSeq.substring(1);
  4186. // Check if it's a hex escape
  4187. var hexMatch = content.match(hexEscapeSequenceRegExp);
  4188. if (hexMatch) {
  4189. var codePoint = parseInt(hexMatch[1], 16);
  4190. // Handle surrogate pairs for code points > 0xFFFF
  4191. if (codePoint > 0xFFFF) {
  4192. // Convert to surrogate pair
  4193. codePoint -= 0x10000;
  4194. var high = 0xD800 + (codePoint >> 10);
  4195. var low = 0xDC00 + (codePoint & 0x3FF);
  4196. return String.fromCharCode(high, low);
  4197. }
  4198. return String.fromCharCode(codePoint);
  4199. }
  4200. // Simple escape - return the character after backslash
  4201. return content[0] || '';
  4202. }
  4203. /**
  4204. * Normalizes attribute selectors by ensuring values are properly quoted with double quotes.
  4205. * Examples:
  4206. * [attr=value] -> [attr="value"]
  4207. * [attr="value"] -> [attr="value"] (unchanged)
  4208. * [attr='value'] -> [attr="value"] (converted to double quotes)
  4209. *
  4210. * @param {string} selector - The selector to normalize
  4211. * @returns {string|null} Normalized selector, or null if invalid
  4212. */
  4213. function normalizeAttributeSelectors(selector) {
  4214. var result = '';
  4215. var i = 0;
  4216. while (i < selector.length) {
  4217. // Look for attribute selector start
  4218. if (selector[i] === '[') {
  4219. result += '[';
  4220. i++;
  4221. var attrContent = '';
  4222. var depth = 1;
  4223. // Find the closing bracket, handling nested brackets and escapes
  4224. while (i < selector.length && depth > 0) {
  4225. if (selector[i] === '\\' && i + 1 < selector.length) {
  4226. attrContent += selector.substring(i, i + 2);
  4227. i += 2;
  4228. continue;
  4229. }
  4230. if (selector[i] === '[') depth++;
  4231. if (selector[i] === ']') {
  4232. depth--;
  4233. if (depth === 0) break;
  4234. }
  4235. attrContent += selector[i];
  4236. i++;
  4237. }
  4238. // Normalize the attribute content
  4239. var normalized = normalizeAttributeContent(attrContent);
  4240. if (normalized === null) {
  4241. // Invalid attribute selector (e.g., unclosed quote)
  4242. return null;
  4243. }
  4244. result += normalized;
  4245. if (i < selector.length && selector[i] === ']') {
  4246. result += ']';
  4247. i++;
  4248. }
  4249. } else {
  4250. result += selector[i];
  4251. i++;
  4252. }
  4253. }
  4254. return result;
  4255. }
  4256. /**
  4257. * Processes a quoted attribute value by checking for proper closure and decoding escape sequences.
  4258. * @param {string} trimmedValue - The quoted value (with quotes)
  4259. * @param {string} quoteChar - The quote character ('"' or "'")
  4260. * @param {string} attrName - The attribute name
  4261. * @param {string} operator - The attribute operator
  4262. * @param {string} flag - Optional case-sensitivity flag
  4263. * @returns {string|null} Normalized attribute content, or null if invalid
  4264. */
  4265. function processQuotedAttributeValue(trimmedValue, quoteChar, attrName, operator, flag) {
  4266. // Check if the closing quote is properly closed (not escaped)
  4267. if (trimmedValue.length < 2) {
  4268. return null; // Too short
  4269. }
  4270. // Find the actual closing quote (not escaped)
  4271. var i = 1;
  4272. var foundClose = false;
  4273. while (i < trimmedValue.length) {
  4274. if (trimmedValue[i] === '\\' && i + 1 < trimmedValue.length) {
  4275. // Skip escape sequence
  4276. var escapeLen = getEscapeSequenceLength(trimmedValue, i);
  4277. i += escapeLen;
  4278. continue;
  4279. }
  4280. if (trimmedValue[i] === quoteChar) {
  4281. // Found closing quote
  4282. foundClose = (i === trimmedValue.length - 1);
  4283. break;
  4284. }
  4285. i++;
  4286. }
  4287. if (!foundClose) {
  4288. return null; // Unclosed quote - invalid
  4289. }
  4290. // Extract inner value and decode escape sequences
  4291. var innerValue = trimmedValue.slice(1, -1);
  4292. var decodedValue = decodeEscapeSequencesInString(innerValue);
  4293. // If decoded value contains quotes, we need to escape them
  4294. var escapedValue = decodedValue.replace(doubleQuoteRegExp, '\\"');
  4295. return attrName + operator + '"' + escapedValue + '"' + (flag ? ' ' + flag : '');
  4296. }
  4297. /**
  4298. * Normalizes the content inside an attribute selector.
  4299. * @param {string} content - The content between [ and ]
  4300. * @returns {string} Normalized content, or null if invalid
  4301. */
  4302. function normalizeAttributeContent(content) {
  4303. // Match: attribute-name [operator] [value] [flag]
  4304. var match = content.match(attributeSelectorContentRegExp);
  4305. if (!match) {
  4306. // No operator (e.g., [disabled]) or malformed - return as is
  4307. return content;
  4308. }
  4309. var attrName = match[1];
  4310. var operator = match[2];
  4311. var valueAndFlag = match[3].trim(); // Trim here instead of in regex
  4312. // Check if there's a case-sensitivity flag (i or s) at the end
  4313. var flagMatch = valueAndFlag.match(attributeCaseFlagRegExp);
  4314. var value = flagMatch ? flagMatch[1] : valueAndFlag;
  4315. var flag = flagMatch ? flagMatch[2] : '';
  4316. // Check for unclosed quotes - this makes the selector invalid
  4317. var trimmedValue = value.trim();
  4318. var firstChar = trimmedValue[0];
  4319. if (firstChar === '"') {
  4320. return processQuotedAttributeValue(trimmedValue, '"', attrName, operator, flag);
  4321. }
  4322. if (firstChar === "'") {
  4323. return processQuotedAttributeValue(trimmedValue, "'", attrName, operator, flag);
  4324. }
  4325. // Check for unescaped special characters in unquoted values
  4326. // Escaped special characters are valid (e.g., \` is valid, but ` is not)
  4327. var hasUnescapedSpecialChar = false;
  4328. for (var i = 0; i < trimmedValue.length; i++) {
  4329. var char = trimmedValue[i];
  4330. if (char === '\\' && i + 1 < trimmedValue.length) {
  4331. // Skip the entire escape sequence
  4332. var escapeLen = getEscapeSequenceLength(trimmedValue, i);
  4333. if (escapeLen > 0) {
  4334. i += escapeLen - 1; // -1 because loop will increment
  4335. continue;
  4336. }
  4337. }
  4338. // Check if this is an unescaped special character
  4339. if (specialCharsNeedEscapeRegExp.test(char)) {
  4340. hasUnescapedSpecialChar = true;
  4341. break;
  4342. }
  4343. }
  4344. if (hasUnescapedSpecialChar) {
  4345. return null; // Unescaped special characters not allowed in unquoted attribute values
  4346. }
  4347. // Decode escape sequences in the value before quoting
  4348. // Inside quotes, special characters don't need escaping
  4349. var decodedValue = decodeEscapeSequencesInString(trimmedValue);
  4350. // If the decoded value contains double quotes, escape them for the output
  4351. // (since we're using double quotes as delimiters)
  4352. var escapedValue = decodedValue.replace(backslashRegExp, '\\\\').replace(doubleQuoteRegExp, '\\"');
  4353. // Unquoted value - add double quotes with decoded and re-escaped content
  4354. return attrName + operator + '"' + escapedValue + '"' + (flag ? ' ' + flag : '');
  4355. }
  4356. /**
  4357. * Processes a CSS selector text
  4358. *
  4359. * @param {string} selectorText - The CSS selector text to process
  4360. * @returns {string} The processed selector text with normalized whitespace and invalid selectors removed
  4361. */
  4362. function processSelectorText(selectorText) {
  4363. // Normalize whitespace first
  4364. var normalized = selectorText.replace(whitespaceNormalizationRegExp, function (match, _, newline) {
  4365. if (newline) return " ";
  4366. return match;
  4367. });
  4368. // Normalize escape sequences to match browser behavior
  4369. normalized = normalizeSelectorEscapes(normalized);
  4370. // Normalize attribute selectors (add quotes to unquoted values)
  4371. // Returns null if invalid (e.g., unclosed quotes)
  4372. normalized = normalizeAttributeSelectors(normalized);
  4373. if (normalized === null) {
  4374. return ''; // Invalid selector - return empty to trigger validation failure
  4375. }
  4376. // Recursively process pseudo-classes to handle nesting
  4377. return processNestedPseudoClasses(normalized);
  4378. }
  4379. /**
  4380. * Recursively processes pseudo-classes to filter invalid selectors
  4381. *
  4382. * @param {string} selectorText - The CSS selector text to process
  4383. * @param {number} depth - Current recursion depth (to prevent infinite loops)
  4384. * @returns {string} The processed selector text with invalid selectors removed
  4385. */
  4386. function processNestedPseudoClasses(selectorText, depth) {
  4387. // Prevent infinite recursion
  4388. if (typeof depth === 'undefined') {
  4389. depth = 0;
  4390. }
  4391. if (depth > 10) {
  4392. return selectorText;
  4393. }
  4394. var pseudoClassMatches = extractPseudoClasses(selectorText);
  4395. // If no pseudo-classes found, return as-is
  4396. if (pseudoClassMatches.length === 0) {
  4397. return selectorText;
  4398. }
  4399. // Build result by processing matches from right to left (to preserve positions)
  4400. var result = selectorText;
  4401. for (var j = pseudoClassMatches.length - 1; j >= 0; j--) {
  4402. var pseudoClass = pseudoClassMatches[j][1];
  4403. if (selectorListPseudoClasses.hasOwnProperty(pseudoClass)) {
  4404. var fullMatch = pseudoClassMatches[j][0];
  4405. var pseudoArgs = pseudoClassMatches[j][2];
  4406. var matchStart = pseudoClassMatches[j][3];
  4407. // Check if ANY selector contains & BEFORE processing
  4408. var nestedSelectorsRaw = parseAndSplitNestedSelectors(pseudoArgs);
  4409. var hasAmpersand = false;
  4410. for (var k = 0; k < nestedSelectorsRaw.length; k++) {
  4411. if (ampersandRegExp.test(nestedSelectorsRaw[k])) {
  4412. hasAmpersand = true;
  4413. break;
  4414. }
  4415. }
  4416. // If & is present, skip all processing (keep everything unchanged)
  4417. if (hasAmpersand) {
  4418. continue;
  4419. }
  4420. // Recursively process the arguments
  4421. var processedArgs = processNestedPseudoClasses(pseudoArgs, depth + 1);
  4422. var nestedSelectors = parseAndSplitNestedSelectors(processedArgs);
  4423. // Filter out invalid selectors
  4424. var validSelectors = [];
  4425. for (var i = 0; i < nestedSelectors.length; i++) {
  4426. var nestedSelector = nestedSelectors[i];
  4427. if (basicSelectorValidator(nestedSelector)) {
  4428. validSelectors.push(nestedSelector);
  4429. }
  4430. }
  4431. // Reconstruct the pseudo-class with only valid selectors
  4432. var newArgs = validSelectors.join(', ');
  4433. var newPseudoClass = ':' + pseudoClass + '(' + newArgs + ')';
  4434. // Replace in the result string using position (processing right to left preserves positions)
  4435. result = result.substring(0, matchStart) + newPseudoClass + result.substring(matchStart + fullMatch.length);
  4436. }
  4437. }
  4438. return result;
  4439. return normalized;
  4440. }
  4441. /**
  4442. * Checks if a selector contains newlines inside quoted strings.
  4443. * Uses iterative parsing to avoid regex backtracking issues.
  4444. * @param {string} selectorText - The selector to check
  4445. * @returns {boolean} True if newlines found inside quotes
  4446. */
  4447. function hasNewlineInQuotedString(selectorText) {
  4448. for (var i = 0; i < selectorText.length; i++) {
  4449. var char = selectorText[i];
  4450. // Start of single-quoted string
  4451. if (char === "'") {
  4452. i++;
  4453. while (i < selectorText.length) {
  4454. if (selectorText[i] === '\\' && i + 1 < selectorText.length) {
  4455. // Skip escape sequence
  4456. i += 2;
  4457. continue;
  4458. }
  4459. if (selectorText[i] === "'") {
  4460. // End of string
  4461. break;
  4462. }
  4463. if (selectorText[i] === '\r' || selectorText[i] === '\n') {
  4464. return true;
  4465. }
  4466. i++;
  4467. }
  4468. }
  4469. // Start of double-quoted string
  4470. else if (char === '"') {
  4471. i++;
  4472. while (i < selectorText.length) {
  4473. if (selectorText[i] === '\\' && i + 1 < selectorText.length) {
  4474. // Skip escape sequence
  4475. i += 2;
  4476. continue;
  4477. }
  4478. if (selectorText[i] === '"') {
  4479. // End of string
  4480. break;
  4481. }
  4482. if (selectorText[i] === '\r' || selectorText[i] === '\n') {
  4483. return true;
  4484. }
  4485. i++;
  4486. }
  4487. }
  4488. }
  4489. return false;
  4490. }
  4491. /**
  4492. * Checks if a given CSS selector text is valid by splitting it by commas
  4493. * and validating each individual selector using the `validateSelector` function.
  4494. *
  4495. * @param {string} selectorText - The CSS selector text to validate. Can contain multiple selectors separated by commas.
  4496. * @returns {boolean} Returns true if all selectors are valid, otherwise false.
  4497. */
  4498. function isValidSelectorText(selectorText) {
  4499. // TODO: The same validations here needs to be reused in CSSStyleRule.selectorText setter
  4500. // TODO: Move these validation logic to a shared function to be reused in CSSStyleRule.selectorText setter
  4501. // Check for empty or whitespace-only selector
  4502. if (!selectorText || selectorText.trim() === '') {
  4503. return false;
  4504. }
  4505. // Check for empty selector lists in pseudo-classes (e.g., :is(), :not(), :where(), :has())
  4506. // These are invalid after filtering out invalid selectors
  4507. if (emptyPseudoClassRegExp.test(selectorText)) {
  4508. return false;
  4509. }
  4510. // Check for newlines inside single or double quotes
  4511. // Uses helper function to avoid regex security issues
  4512. if (hasNewlineInQuotedString(selectorText)) {
  4513. return false;
  4514. }
  4515. // Split selectorText by commas and validate each part
  4516. var selectors = parseAndSplitNestedSelectors(selectorText);
  4517. for (var i = 0; i < selectors.length; i++) {
  4518. var selector = selectors[i].trim();
  4519. if (!validateSelector(selector) || !validateNamespaceSelector(selector)) {
  4520. return false;
  4521. }
  4522. }
  4523. return true;
  4524. }
  4525. function pushToAncestorRules(rule) {
  4526. ancestorRules.push(rule);
  4527. }
  4528. function parseError(message, isNested) {
  4529. var lines = token.substring(0, i).split('\n');
  4530. var lineCount = lines.length;
  4531. var charCount = lines.pop().length + 1;
  4532. var error = new Error(message + ' (line ' + lineCount + ', char ' + charCount + ')');
  4533. error.line = lineCount;
  4534. /* jshint sub : true */
  4535. error['char'] = charCount;
  4536. error.styleSheet = styleSheet;
  4537. error.isNested = !!isNested;
  4538. // Print the error but continue parsing the sheet
  4539. try {
  4540. throw error;
  4541. } catch (e) {
  4542. errorHandler && errorHandler(e);
  4543. }
  4544. };
  4545. /**
  4546. * Handles invalid selectors with unmatched quotes by skipping the entire rule block.
  4547. * @param {string} nextState - The parser state to transition to after skipping
  4548. */
  4549. function handleUnmatchedQuoteInSelector(nextState) {
  4550. // parseError('Invalid selector with unmatched quote: ' + buffer.trim());
  4551. // Skip this entire invalid rule including its block
  4552. var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
  4553. if (ruleClosingMatch) {
  4554. i += ruleClosingMatch.index + ruleClosingMatch[0].length - 1;
  4555. }
  4556. styleRule = null;
  4557. buffer = "";
  4558. hasUnmatchedQuoteInSelector = false; // Reset flag
  4559. state = nextState;
  4560. }
  4561. // Helper functions to check character types
  4562. function isSelectorStartChar(char) {
  4563. return '.:#&*['.indexOf(char) !== -1;
  4564. }
  4565. function isWhitespaceChar(char) {
  4566. return ' \t\n\r'.indexOf(char) !== -1;
  4567. }
  4568. // Helper functions for character type checking (faster than regex for single chars)
  4569. function isDigit(char) {
  4570. var code = char.charCodeAt(0);
  4571. return code >= 0x0030 && code <= 0x0039; // 0-9
  4572. }
  4573. function isHexDigit(char) {
  4574. if (!char) return false;
  4575. var code = char.charCodeAt(0);
  4576. return (code >= 0x0030 && code <= 0x0039) || // 0-9
  4577. (code >= 0x0041 && code <= 0x0046) || // A-F
  4578. (code >= 0x0061 && code <= 0x0066); // a-f
  4579. }
  4580. function isLetter(char) {
  4581. if (!char) return false;
  4582. var code = char.charCodeAt(0);
  4583. return (code >= 0x0041 && code <= 0x005A) || // A-Z
  4584. (code >= 0x0061 && code <= 0x007A); // a-z
  4585. }
  4586. function isAlphanumeric(char) {
  4587. var code = char.charCodeAt(0);
  4588. return (code >= 0x0030 && code <= 0x0039) || // 0-9
  4589. (code >= 0x0041 && code <= 0x005A) || // A-Z
  4590. (code >= 0x0061 && code <= 0x007A); // a-z
  4591. }
  4592. /**
  4593. * Get the length of an escape sequence starting at the given position.
  4594. * CSS escape sequences are:
  4595. * - Backslash followed by 1-6 hex digits, optionally followed by a whitespace (consumed)
  4596. * - Backslash followed by any non-hex character
  4597. * @param {string} str - The string to check
  4598. * @param {number} pos - Position of the backslash
  4599. * @returns {number} Number of characters in the escape sequence (including backslash)
  4600. */
  4601. function getEscapeSequenceLength(str, pos) {
  4602. if (str[pos] !== '\\' || pos + 1 >= str.length) {
  4603. return 0;
  4604. }
  4605. var nextChar = str[pos + 1];
  4606. // Check if it's a hex escape
  4607. if (isHexDigit(nextChar)) {
  4608. var hexLength = 1;
  4609. // Count up to 6 hex digits
  4610. while (hexLength < 6 && pos + 1 + hexLength < str.length && isHexDigit(str[pos + 1 + hexLength])) {
  4611. hexLength++;
  4612. }
  4613. // Check if followed by optional whitespace (which gets consumed)
  4614. if (pos + 1 + hexLength < str.length && isWhitespaceChar(str[pos + 1 + hexLength])) {
  4615. return 1 + hexLength + 1; // backslash + hex digits + whitespace
  4616. }
  4617. return 1 + hexLength; // backslash + hex digits
  4618. }
  4619. // Simple escape: backslash + any character
  4620. return 2;
  4621. }
  4622. /**
  4623. * Check if a string contains an unescaped occurrence of a specific character
  4624. * @param {string} str - The string to search
  4625. * @param {string} char - The character to look for
  4626. * @returns {boolean} True if the character appears unescaped
  4627. */
  4628. function containsUnescaped(str, char) {
  4629. for (var i = 0; i < str.length; i++) {
  4630. if (str[i] === '\\') {
  4631. var escapeLen = getEscapeSequenceLength(str, i);
  4632. if (escapeLen > 0) {
  4633. i += escapeLen - 1; // -1 because loop will increment
  4634. continue;
  4635. }
  4636. }
  4637. if (str[i] === char) {
  4638. return true;
  4639. }
  4640. }
  4641. return false;
  4642. }
  4643. var endingIndex = token.length - 1;
  4644. var initialEndingIndex = endingIndex;
  4645. for (var character; (character = token.charAt(i)); i++) {
  4646. if (i === endingIndex) {
  4647. switch (state) {
  4648. case "importRule":
  4649. case "namespaceRule":
  4650. case "layerBlock":
  4651. if (character !== ";") {
  4652. token += ";";
  4653. endingIndex += 1;
  4654. }
  4655. break;
  4656. case "value":
  4657. if (character !== "}") {
  4658. if (character === ";") {
  4659. token += "}"
  4660. } else {
  4661. token += ";";
  4662. }
  4663. endingIndex += 1;
  4664. break;
  4665. }
  4666. case "name":
  4667. case "before-name":
  4668. if (character === "}") {
  4669. token += " "
  4670. } else {
  4671. token += "}"
  4672. }
  4673. endingIndex += 1
  4674. break;
  4675. case "before-selector":
  4676. if (character !== "}" && currentScope !== styleSheet) {
  4677. token += "}"
  4678. endingIndex += 1
  4679. break;
  4680. }
  4681. }
  4682. }
  4683. // Handle escape sequences before processing special characters
  4684. // CSS escape sequences: \HHHHHH (1-6 hex digits) optionally followed by whitespace, or \ + any char
  4685. if (character === '\\' && i + 1 < token.length) {
  4686. var escapeLen = getEscapeSequenceLength(token, i);
  4687. if (escapeLen > 0) {
  4688. buffer += token.substr(i, escapeLen);
  4689. i += escapeLen - 1; // -1 because loop will increment
  4690. continue;
  4691. }
  4692. }
  4693. switch (character) {
  4694. case " ":
  4695. case "\t":
  4696. case "\r":
  4697. case "\n":
  4698. case "\f":
  4699. if (SIGNIFICANT_WHITESPACE[state]) {
  4700. buffer += character;
  4701. }
  4702. break;
  4703. // String
  4704. case '"':
  4705. index = i + 1;
  4706. do {
  4707. index = token.indexOf('"', index) + 1;
  4708. if (!index) {
  4709. parseError('Unmatched "');
  4710. // If we're parsing a selector, flag it as invalid
  4711. if (state === "selector" || state === "atRule") {
  4712. hasUnmatchedQuoteInSelector = true;
  4713. }
  4714. }
  4715. } while (token[index - 2] === '\\');
  4716. if (index === 0) {
  4717. break;
  4718. }
  4719. buffer += token.slice(i, index);
  4720. i = index - 1;
  4721. switch (state) {
  4722. case 'before-value':
  4723. state = 'value';
  4724. break;
  4725. case 'importRule-begin':
  4726. state = 'importRule';
  4727. if (i === endingIndex) {
  4728. token += ';'
  4729. }
  4730. break;
  4731. case 'namespaceRule-begin':
  4732. state = 'namespaceRule';
  4733. if (i === endingIndex) {
  4734. token += ';'
  4735. }
  4736. break;
  4737. }
  4738. break;
  4739. case "'":
  4740. index = i + 1;
  4741. do {
  4742. index = token.indexOf("'", index) + 1;
  4743. if (!index) {
  4744. parseError("Unmatched '");
  4745. // If we're parsing a selector, flag it as invalid
  4746. if (state === "selector" || state === "atRule") {
  4747. hasUnmatchedQuoteInSelector = true;
  4748. }
  4749. }
  4750. } while (token[index - 2] === '\\');
  4751. if (index === 0) {
  4752. break;
  4753. }
  4754. buffer += token.slice(i, index);
  4755. i = index - 1;
  4756. switch (state) {
  4757. case 'before-value':
  4758. state = 'value';
  4759. break;
  4760. case 'importRule-begin':
  4761. state = 'importRule';
  4762. break;
  4763. case 'namespaceRule-begin':
  4764. state = 'namespaceRule';
  4765. break;
  4766. }
  4767. break;
  4768. // Comment
  4769. case "/":
  4770. if (token.charAt(i + 1) === "*") {
  4771. i += 2;
  4772. index = token.indexOf("*/", i);
  4773. if (index === -1) {
  4774. i = token.length - 1;
  4775. buffer = "";
  4776. } else {
  4777. i = index + 1;
  4778. }
  4779. } else {
  4780. buffer += character;
  4781. }
  4782. if (state === "importRule-begin") {
  4783. buffer += " ";
  4784. state = "importRule";
  4785. }
  4786. if (state === "namespaceRule-begin") {
  4787. buffer += " ";
  4788. state = "namespaceRule";
  4789. }
  4790. break;
  4791. // At-rule
  4792. case "@":
  4793. if (nestedSelectorRule) {
  4794. if (styleRule && styleRule.constructor.name === "CSSNestedDeclarations") {
  4795. currentScope.cssRules.push(styleRule);
  4796. }
  4797. // Only reset styleRule to parent if styleRule is not the nestedSelectorRule itself
  4798. // This preserves nested selectors when followed immediately by @-rules
  4799. if (styleRule !== nestedSelectorRule && nestedSelectorRule.parentRule && nestedSelectorRule.parentRule.constructor.name === "CSSStyleRule") {
  4800. styleRule = nestedSelectorRule.parentRule;
  4801. }
  4802. // Don't reset nestedSelectorRule here - preserve it through @-rules
  4803. }
  4804. if (token.indexOf("@-moz-document", i) === i) {
  4805. validateAtRule("@-moz-document", function () {
  4806. state = "documentRule-begin";
  4807. documentRule = new CSSOM.CSSDocumentRule();
  4808. documentRule.__starts = i;
  4809. i += "-moz-document".length;
  4810. });
  4811. buffer = "";
  4812. break;
  4813. } else if (token.indexOf("@media", i) === i) {
  4814. validateAtRule("@media", function () {
  4815. state = "atBlock";
  4816. mediaRule = new CSSOM.CSSMediaRule();
  4817. mediaRule.__starts = i;
  4818. i += "media".length;
  4819. });
  4820. buffer = "";
  4821. break;
  4822. } else if (token.indexOf("@container", i) === i) {
  4823. validateAtRule("@container", function () {
  4824. state = "containerBlock";
  4825. containerRule = new CSSOM.CSSContainerRule();
  4826. containerRule.__starts = i;
  4827. i += "container".length;
  4828. });
  4829. buffer = "";
  4830. break;
  4831. } else if (token.indexOf("@counter-style", i) === i) {
  4832. buffer = "";
  4833. // @counter-style can be nested only inside CSSScopeRule or CSSConditionRule
  4834. // and only if there's no CSSStyleRule in the parent chain
  4835. var cannotBeNested = !canAtRuleBeNested();
  4836. validateAtRule("@counter-style", function () {
  4837. state = "counterStyleBlock"
  4838. counterStyleRule = new CSSOM.CSSCounterStyleRule();
  4839. counterStyleRule.__starts = i;
  4840. i += "counter-style".length;
  4841. }, cannotBeNested);
  4842. break;
  4843. } else if (token.indexOf("@property", i) === i) {
  4844. buffer = "";
  4845. // @property can be nested only inside CSSScopeRule or CSSConditionRule
  4846. // and only if there's no CSSStyleRule in the parent chain
  4847. var cannotBeNested = !canAtRuleBeNested();
  4848. validateAtRule("@property", function () {
  4849. state = "propertyBlock"
  4850. propertyRule = new CSSOM.CSSPropertyRule();
  4851. propertyRule.__starts = i;
  4852. i += "property".length;
  4853. }, cannotBeNested);
  4854. break;
  4855. } else if (token.indexOf("@scope", i) === i) {
  4856. validateAtRule("@scope", function () {
  4857. state = "scopeBlock";
  4858. scopeRule = new CSSOM.CSSScopeRule();
  4859. scopeRule.__starts = i;
  4860. i += "scope".length;
  4861. });
  4862. buffer = "";
  4863. break;
  4864. } else if (token.indexOf("@layer", i) === i) {
  4865. validateAtRule("@layer", function () {
  4866. state = "layerBlock"
  4867. layerBlockRule = new CSSOM.CSSLayerBlockRule();
  4868. layerBlockRule.__starts = i;
  4869. i += "layer".length;
  4870. });
  4871. buffer = "";
  4872. break;
  4873. } else if (token.indexOf("@page", i) === i) {
  4874. validateAtRule("@page", function () {
  4875. state = "pageBlock"
  4876. pageRule = new CSSOM.CSSPageRule();
  4877. pageRule.__starts = i;
  4878. i += "page".length;
  4879. });
  4880. buffer = "";
  4881. break;
  4882. } else if (token.indexOf("@supports", i) === i) {
  4883. validateAtRule("@supports", function () {
  4884. state = "conditionBlock";
  4885. supportsRule = new CSSOM.CSSSupportsRule();
  4886. supportsRule.__starts = i;
  4887. i += "supports".length;
  4888. });
  4889. buffer = "";
  4890. break;
  4891. } else if (token.indexOf("@host", i) === i) {
  4892. validateAtRule("@host", function () {
  4893. state = "hostRule-begin";
  4894. i += "host".length;
  4895. hostRule = new CSSOM.CSSHostRule();
  4896. hostRule.__starts = i;
  4897. });
  4898. buffer = "";
  4899. break;
  4900. } else if (token.indexOf("@starting-style", i) === i) {
  4901. validateAtRule("@starting-style", function () {
  4902. state = "startingStyleRule-begin";
  4903. i += "starting-style".length;
  4904. startingStyleRule = new CSSOM.CSSStartingStyleRule();
  4905. startingStyleRule.__starts = i;
  4906. });
  4907. buffer = "";
  4908. break;
  4909. } else if (token.indexOf("@import", i) === i) {
  4910. buffer = "";
  4911. validateAtRule("@import", function () {
  4912. state = "importRule-begin";
  4913. i += "import".length;
  4914. buffer += "@import";
  4915. }, true);
  4916. break;
  4917. } else if (token.indexOf("@namespace", i) === i) {
  4918. buffer = "";
  4919. validateAtRule("@namespace", function () {
  4920. state = "namespaceRule-begin";
  4921. i += "namespace".length;
  4922. buffer += "@namespace";
  4923. }, true);
  4924. break;
  4925. } else if (token.indexOf("@font-face", i) === i) {
  4926. buffer = "";
  4927. // @font-face can be nested only inside CSSScopeRule or CSSConditionRule
  4928. // and only if there's no CSSStyleRule in the parent chain
  4929. var cannotBeNested = !canAtRuleBeNested();
  4930. validateAtRule("@font-face", function () {
  4931. state = "fontFaceRule-begin";
  4932. i += "font-face".length;
  4933. fontFaceRule = new CSSOM.CSSFontFaceRule();
  4934. fontFaceRule.__starts = i;
  4935. }, cannotBeNested);
  4936. break;
  4937. } else {
  4938. // Reset lastIndex before using global regex (shared instance)
  4939. atKeyframesRegExp.lastIndex = i;
  4940. var matchKeyframes = atKeyframesRegExp.exec(token);
  4941. if (matchKeyframes && matchKeyframes.index === i) {
  4942. state = "keyframesRule-begin";
  4943. keyframesRule = new CSSOM.CSSKeyframesRule();
  4944. keyframesRule.__starts = i;
  4945. keyframesRule._vendorPrefix = matchKeyframes[1]; // Will come out as undefined if no prefix was found
  4946. i += matchKeyframes[0].length - 1;
  4947. buffer = "";
  4948. break;
  4949. } else if (state === "selector") {
  4950. state = "atRule";
  4951. }
  4952. }
  4953. buffer += character;
  4954. break;
  4955. case "{":
  4956. if (currentScope === topScope) {
  4957. nestedSelectorRule = null;
  4958. }
  4959. if (state === 'before-selector') {
  4960. parseError("Unexpected {");
  4961. i = ignoreBalancedBlock(i, token.slice(i));
  4962. break;
  4963. }
  4964. if (state === "selector" || state === "atRule") {
  4965. if (!nestedSelectorRule && containsUnescaped(buffer, ";")) {
  4966. var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
  4967. if (ruleClosingMatch) {
  4968. styleRule = null;
  4969. buffer = "";
  4970. state = "before-selector";
  4971. i += ruleClosingMatch.index + ruleClosingMatch[0].length;
  4972. break;
  4973. }
  4974. }
  4975. // Ensure styleRule exists before trying to set properties on it
  4976. if (!styleRule) {
  4977. styleRule = new CSSOM.CSSStyleRule();
  4978. styleRule.__starts = i;
  4979. }
  4980. // Check if tokenizer detected an unmatched quote BEFORE setting up the rule
  4981. if (hasUnmatchedQuoteInSelector) {
  4982. handleUnmatchedQuoteInSelector("before-selector");
  4983. break;
  4984. }
  4985. var originalParentRule = parentRule;
  4986. if (parentRule) {
  4987. styleRule.__parentRule = parentRule;
  4988. pushToAncestorRules(parentRule);
  4989. }
  4990. currentScope = parentRule = styleRule;
  4991. var processedSelectorText = processSelectorText(buffer.trim());
  4992. // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
  4993. if (originalParentRule && originalParentRule.constructor.name === "CSSStyleRule") {
  4994. styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function (sel) {
  4995. // Add & at the beginning if there's no & in the selector, or if it starts with a combinator
  4996. return (sel.indexOf('&') === -1 || startsWithCombinatorRegExp.test(sel)) ? '& ' + sel : sel;
  4997. }).join(', ');
  4998. } else {
  4999. // Normalize comma spacing: split by commas and rejoin with ", "
  5000. styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).join(', ');
  5001. }
  5002. styleRule.style.__starts = i;
  5003. styleRule.__parentStyleSheet = styleSheet;
  5004. buffer = "";
  5005. state = "before-name";
  5006. } else if (state === "atBlock") {
  5007. mediaRule.media.mediaText = buffer.trim();
  5008. if (parentRule) {
  5009. mediaRule.__parentRule = parentRule;
  5010. pushToAncestorRules(parentRule);
  5011. // If entering @media from within a CSSStyleRule, set nestedSelectorRule
  5012. // so that & selectors and declarations work correctly inside
  5013. if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
  5014. nestedSelectorRule = parentRule;
  5015. }
  5016. }
  5017. currentScope = parentRule = mediaRule;
  5018. pushToAncestorRules(mediaRule);
  5019. mediaRule.__parentStyleSheet = styleSheet;
  5020. // Don't reset styleRule to null if it's a nested CSSStyleRule that will contain this @-rule
  5021. if (!styleRule || styleRule.constructor.name !== "CSSStyleRule" || !styleRule.__parentRule) {
  5022. styleRule = null; // Reset styleRule when entering @-rule
  5023. }
  5024. buffer = "";
  5025. state = "before-selector";
  5026. } else if (state === "containerBlock") {
  5027. containerRule.__conditionText = buffer.trim();
  5028. if (parentRule) {
  5029. containerRule.__parentRule = parentRule;
  5030. pushToAncestorRules(parentRule);
  5031. if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
  5032. nestedSelectorRule = parentRule;
  5033. }
  5034. }
  5035. currentScope = parentRule = containerRule;
  5036. pushToAncestorRules(containerRule);
  5037. containerRule.__parentStyleSheet = styleSheet;
  5038. styleRule = null; // Reset styleRule when entering @-rule
  5039. buffer = "";
  5040. state = "before-selector";
  5041. } else if (state === "counterStyleBlock") {
  5042. var counterStyleName = buffer.trim().replace(newlineRemovalRegExp, "");
  5043. // Validate: name cannot be empty, contain whitespace, or contain dots
  5044. var isValidCounterStyleName = counterStyleName.length > 0 && !whitespaceAndDotRegExp.test(counterStyleName);
  5045. if (isValidCounterStyleName) {
  5046. counterStyleRule.name = counterStyleName;
  5047. if (parentRule) {
  5048. counterStyleRule.__parentRule = parentRule;
  5049. }
  5050. counterStyleRule.__parentStyleSheet = styleSheet;
  5051. styleRule = counterStyleRule;
  5052. }
  5053. buffer = "";
  5054. } else if (state === "propertyBlock") {
  5055. var propertyName = buffer.trim().replace(newlineRemovalRegExp, "");
  5056. // Validate: name must start with -- (custom property)
  5057. var isValidPropertyName = propertyName.indexOf("--") === 0;
  5058. if (isValidPropertyName) {
  5059. propertyRule.__name = propertyName;
  5060. if (parentRule) {
  5061. propertyRule.__parentRule = parentRule;
  5062. }
  5063. propertyRule.__parentStyleSheet = styleSheet;
  5064. styleRule = propertyRule;
  5065. }
  5066. buffer = "";
  5067. } else if (state === "conditionBlock") {
  5068. supportsRule.__conditionText = buffer.trim();
  5069. if (parentRule) {
  5070. supportsRule.__parentRule = parentRule;
  5071. pushToAncestorRules(parentRule);
  5072. if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
  5073. nestedSelectorRule = parentRule;
  5074. }
  5075. }
  5076. currentScope = parentRule = supportsRule;
  5077. pushToAncestorRules(supportsRule);
  5078. supportsRule.__parentStyleSheet = styleSheet;
  5079. styleRule = null; // Reset styleRule when entering @-rule
  5080. buffer = "";
  5081. state = "before-selector";
  5082. } else if (state === "scopeBlock") {
  5083. var parsedScopePrelude = parseScopePrelude(buffer.trim());
  5084. if (parsedScopePrelude.hasStart) {
  5085. scopeRule.__start = parsedScopePrelude.startSelector;
  5086. }
  5087. if (parsedScopePrelude.hasEnd) {
  5088. scopeRule.__end = parsedScopePrelude.endSelector;
  5089. }
  5090. if (parsedScopePrelude.hasOnlyEnd) {
  5091. scopeRule.__end = parsedScopePrelude.endSelector;
  5092. }
  5093. if (parentRule) {
  5094. scopeRule.__parentRule = parentRule;
  5095. pushToAncestorRules(parentRule);
  5096. if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
  5097. nestedSelectorRule = parentRule;
  5098. }
  5099. }
  5100. currentScope = parentRule = scopeRule;
  5101. pushToAncestorRules(scopeRule);
  5102. scopeRule.__parentStyleSheet = styleSheet;
  5103. styleRule = null; // Reset styleRule when entering @-rule
  5104. buffer = "";
  5105. state = "before-selector";
  5106. } else if (state === "layerBlock") {
  5107. layerBlockRule.name = buffer.trim();
  5108. var isValidName = layerBlockRule.name.length === 0 || layerBlockRule.name.match(cssCustomIdentifierRegExp) !== null;
  5109. if (isValidName) {
  5110. if (parentRule) {
  5111. layerBlockRule.__parentRule = parentRule;
  5112. pushToAncestorRules(parentRule);
  5113. if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
  5114. nestedSelectorRule = parentRule;
  5115. }
  5116. }
  5117. currentScope = parentRule = layerBlockRule;
  5118. pushToAncestorRules(layerBlockRule);
  5119. layerBlockRule.__parentStyleSheet = styleSheet;
  5120. }
  5121. styleRule = null; // Reset styleRule when entering @-rule
  5122. buffer = "";
  5123. state = "before-selector";
  5124. } else if (state === "pageBlock") {
  5125. pageRule.selectorText = buffer.trim();
  5126. if (parentRule) {
  5127. pageRule.__parentRule = parentRule;
  5128. pushToAncestorRules(parentRule);
  5129. }
  5130. currentScope = parentRule = pageRule;
  5131. pageRule.__parentStyleSheet = styleSheet;
  5132. styleRule = pageRule;
  5133. buffer = "";
  5134. state = "before-name";
  5135. } else if (state === "hostRule-begin") {
  5136. if (parentRule) {
  5137. pushToAncestorRules(parentRule);
  5138. }
  5139. currentScope = parentRule = hostRule;
  5140. pushToAncestorRules(hostRule);
  5141. hostRule.__parentStyleSheet = styleSheet;
  5142. buffer = "";
  5143. state = "before-selector";
  5144. } else if (state === "startingStyleRule-begin") {
  5145. if (parentRule) {
  5146. startingStyleRule.__parentRule = parentRule;
  5147. pushToAncestorRules(parentRule);
  5148. if (parentRule.constructor.name === "CSSStyleRule" && !nestedSelectorRule) {
  5149. nestedSelectorRule = parentRule;
  5150. }
  5151. }
  5152. currentScope = parentRule = startingStyleRule;
  5153. pushToAncestorRules(startingStyleRule);
  5154. startingStyleRule.__parentStyleSheet = styleSheet;
  5155. styleRule = null; // Reset styleRule when entering @-rule
  5156. buffer = "";
  5157. state = "before-selector";
  5158. } else if (state === "fontFaceRule-begin") {
  5159. if (parentRule) {
  5160. fontFaceRule.__parentRule = parentRule;
  5161. }
  5162. fontFaceRule.__parentStyleSheet = styleSheet;
  5163. styleRule = fontFaceRule;
  5164. buffer = "";
  5165. state = "before-name";
  5166. } else if (state === "keyframesRule-begin") {
  5167. keyframesRule.name = buffer.trim();
  5168. if (parentRule) {
  5169. pushToAncestorRules(parentRule);
  5170. keyframesRule.__parentRule = parentRule;
  5171. }
  5172. keyframesRule.__parentStyleSheet = styleSheet;
  5173. currentScope = parentRule = keyframesRule;
  5174. buffer = "";
  5175. state = "keyframeRule-begin";
  5176. } else if (state === "keyframeRule-begin") {
  5177. styleRule = new CSSOM.CSSKeyframeRule();
  5178. styleRule.keyText = buffer.trim();
  5179. styleRule.__starts = i;
  5180. buffer = "";
  5181. state = "before-name";
  5182. } else if (state === "documentRule-begin") {
  5183. // FIXME: what if this '{' is in the url text of the match function?
  5184. documentRule.matcher.matcherText = buffer.trim();
  5185. if (parentRule) {
  5186. pushToAncestorRules(parentRule);
  5187. documentRule.__parentRule = parentRule;
  5188. }
  5189. currentScope = parentRule = documentRule;
  5190. pushToAncestorRules(documentRule);
  5191. documentRule.__parentStyleSheet = styleSheet;
  5192. buffer = "";
  5193. state = "before-selector";
  5194. } else if (state === "before-name" || state === "name") {
  5195. // @font-face and similar rules don't support nested selectors
  5196. // If we encounter a nested selector block inside them, skip it
  5197. if (styleRule.constructor.name === "CSSFontFaceRule" ||
  5198. styleRule.constructor.name === "CSSKeyframeRule" ||
  5199. (styleRule.constructor.name === "CSSPageRule" && parentRule === styleRule)) {
  5200. // Skip the nested block
  5201. var ruleClosingMatch = token.slice(i).match(forwardRuleClosingBraceRegExp);
  5202. if (ruleClosingMatch) {
  5203. i += ruleClosingMatch.index + ruleClosingMatch[0].length - 1;
  5204. buffer = "";
  5205. state = "before-name";
  5206. break;
  5207. }
  5208. }
  5209. if (styleRule.constructor.name === "CSSNestedDeclarations") {
  5210. if (styleRule.style.length) {
  5211. parentRule.cssRules.push(styleRule);
  5212. styleRule.__parentRule = parentRule;
  5213. styleRule.__parentStyleSheet = styleSheet;
  5214. pushToAncestorRules(parentRule);
  5215. } else {
  5216. // If the styleRule is empty, we can assume that it's a nested selector
  5217. pushToAncestorRules(parentRule);
  5218. }
  5219. } else {
  5220. currentScope = parentRule = styleRule;
  5221. pushToAncestorRules(parentRule);
  5222. styleRule.__parentStyleSheet = styleSheet;
  5223. }
  5224. styleRule = new CSSOM.CSSStyleRule();
  5225. // Check if tokenizer detected an unmatched quote BEFORE setting up the rule
  5226. if (hasUnmatchedQuoteInSelector) {
  5227. handleUnmatchedQuoteInSelector("before-name");
  5228. break;
  5229. }
  5230. var processedSelectorText = processSelectorText(buffer.trim());
  5231. // In a nested selector, ensure each selector contains '&' at the beginning, except for selectors that already have '&' somewhere
  5232. if (parentRule.constructor.name === "CSSScopeRule" || (parentRule.constructor.name !== "CSSStyleRule" && parentRule.parentRule === null)) {
  5233. // Normalize comma spacing: split by commas and rejoin with ", "
  5234. styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).join(', ');
  5235. } else {
  5236. styleRule.selectorText = parseAndSplitNestedSelectors(processedSelectorText).map(function (sel) {
  5237. // Add & at the beginning if there's no & in the selector, or if it starts with a combinator
  5238. return (sel.indexOf('&') === -1 || startsWithCombinatorRegExp.test(sel)) ? '& ' + sel : sel;
  5239. }).join(', ');
  5240. }
  5241. styleRule.style.__starts = i - buffer.length;
  5242. styleRule.__parentRule = parentRule;
  5243. // Only set nestedSelectorRule if we're directly inside a CSSStyleRule or CSSScopeRule,
  5244. // not inside other grouping rules like @media/@supports
  5245. if (parentRule.constructor.name === "CSSStyleRule" || parentRule.constructor.name === "CSSScopeRule") {
  5246. nestedSelectorRule = styleRule;
  5247. }
  5248. // Set __parentStyleSheet for the new nested styleRule
  5249. styleRule.__parentStyleSheet = styleSheet;
  5250. // Update currentScope and parentRule to the new nested styleRule
  5251. // so that subsequent content (like @-rules) will be children of this rule
  5252. currentScope = parentRule = styleRule;
  5253. buffer = "";
  5254. state = "before-name";
  5255. }
  5256. break;
  5257. case ":":
  5258. if (state === "name") {
  5259. // It can be a nested selector, let's check
  5260. var openBraceBeforeMatch = token.slice(i).match(declarationOrOpenBraceRegExp);
  5261. var hasOpenBraceBefore = openBraceBeforeMatch && openBraceBeforeMatch[0] === '{';
  5262. if (hasOpenBraceBefore) {
  5263. // Is a selector
  5264. buffer += character;
  5265. } else {
  5266. // Is a declaration
  5267. name = buffer.trim();
  5268. buffer = "";
  5269. state = "before-value";
  5270. }
  5271. } else {
  5272. buffer += character;
  5273. }
  5274. break;
  5275. case "(":
  5276. if (state === 'value') {
  5277. // ie css expression mode
  5278. if (buffer.trim() === 'expression') {
  5279. var info = (new CSSOM.CSSValueExpression(token, i)).parse();
  5280. if (info.error) {
  5281. parseError(info.error);
  5282. } else {
  5283. buffer += info.expression;
  5284. i = info.idx;
  5285. }
  5286. } else {
  5287. state = 'value-parenthesis';
  5288. //always ensure this is reset to 1 on transition
  5289. //from value to value-parenthesis
  5290. valueParenthesisDepth = 1;
  5291. buffer += character;
  5292. }
  5293. } else if (state === 'value-parenthesis') {
  5294. valueParenthesisDepth++;
  5295. buffer += character;
  5296. } else {
  5297. buffer += character;
  5298. }
  5299. break;
  5300. case ")":
  5301. if (state === 'value-parenthesis') {
  5302. valueParenthesisDepth--;
  5303. if (valueParenthesisDepth === 0) state = 'value';
  5304. }
  5305. buffer += character;
  5306. break;
  5307. case "!":
  5308. if (state === "value" && token.indexOf("!important", i) === i) {
  5309. priority = "important";
  5310. i += "important".length;
  5311. } else {
  5312. buffer += character;
  5313. }
  5314. break;
  5315. case ";":
  5316. switch (state) {
  5317. case "before-value":
  5318. case "before-name":
  5319. parseError("Unexpected ;");
  5320. buffer = "";
  5321. state = "before-name";
  5322. break;
  5323. case "value":
  5324. styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
  5325. priority = "";
  5326. buffer = "";
  5327. state = "before-name";
  5328. break;
  5329. case "atRule":
  5330. buffer = "";
  5331. state = "before-selector";
  5332. break;
  5333. case "importRule":
  5334. var isValid = topScope.cssRules.length === 0 || topScope.cssRules.some(function (rule) {
  5335. return ['CSSImportRule', 'CSSLayerStatementRule'].indexOf(rule.constructor.name) !== -1
  5336. });
  5337. if (isValid) {
  5338. importRule = new CSSOM.CSSImportRule();
  5339. if (opts && opts.globalObject && opts.globalObject.CSSStyleSheet) {
  5340. importRule.__styleSheet = new opts.globalObject.CSSStyleSheet();
  5341. }
  5342. importRule.styleSheet.__constructed = false;
  5343. importRule.__parentStyleSheet = importRule.styleSheet.__parentStyleSheet = styleSheet;
  5344. importRule.parse(buffer + character);
  5345. topScope.cssRules.push(importRule);
  5346. }
  5347. buffer = "";
  5348. state = "before-selector";
  5349. break;
  5350. case "namespaceRule":
  5351. var isValid = topScope.cssRules.length === 0 || topScope.cssRules.every(function (rule) {
  5352. return ['CSSImportRule', 'CSSLayerStatementRule', 'CSSNamespaceRule'].indexOf(rule.constructor.name) !== -1
  5353. });
  5354. if (isValid) {
  5355. try {
  5356. // Validate namespace syntax before creating the rule
  5357. var testNamespaceRule = new CSSOM.CSSNamespaceRule();
  5358. testNamespaceRule.parse(buffer + character);
  5359. namespaceRule = testNamespaceRule;
  5360. namespaceRule.__parentStyleSheet = styleSheet;
  5361. topScope.cssRules.push(namespaceRule);
  5362. // Track the namespace prefix for validation
  5363. if (namespaceRule.prefix) {
  5364. definedNamespacePrefixes[namespaceRule.prefix] = namespaceRule.namespaceURI;
  5365. }
  5366. } catch (e) {
  5367. parseError(e.message);
  5368. }
  5369. }
  5370. buffer = "";
  5371. state = "before-selector";
  5372. break;
  5373. case "layerBlock":
  5374. var nameListStr = buffer.trim().split(",").map(function (name) {
  5375. return name.trim();
  5376. });
  5377. var isInvalid = nameListStr.some(function (name) {
  5378. return name.trim().match(cssCustomIdentifierRegExp) === null;
  5379. });
  5380. // Check if there's a CSSStyleRule in the parent chain
  5381. var hasStyleRuleParent = false;
  5382. if (parentRule) {
  5383. var checkParent = parentRule;
  5384. while (checkParent) {
  5385. if (checkParent.constructor.name === "CSSStyleRule") {
  5386. hasStyleRuleParent = true;
  5387. break;
  5388. }
  5389. checkParent = checkParent.__parentRule;
  5390. }
  5391. }
  5392. if (!isInvalid && !hasStyleRuleParent) {
  5393. layerStatementRule = new CSSOM.CSSLayerStatementRule();
  5394. layerStatementRule.__parentStyleSheet = styleSheet;
  5395. layerStatementRule.__starts = layerBlockRule.__starts;
  5396. layerStatementRule.__ends = i;
  5397. layerStatementRule.nameList = nameListStr;
  5398. // Add to parent rule if nested, otherwise to top scope
  5399. if (parentRule) {
  5400. layerStatementRule.__parentRule = parentRule;
  5401. parentRule.cssRules.push(layerStatementRule);
  5402. } else {
  5403. topScope.cssRules.push(layerStatementRule);
  5404. }
  5405. }
  5406. buffer = "";
  5407. state = "before-selector";
  5408. break;
  5409. default:
  5410. buffer += character;
  5411. break;
  5412. }
  5413. break;
  5414. case "}":
  5415. if (state === "counterStyleBlock") {
  5416. // FIXME : Implement missing properties on CSSCounterStyleRule interface and update parse method
  5417. // For now it's just assigning entire rule text
  5418. if (counterStyleRule.name) {
  5419. // Only process if name was set (valid)
  5420. counterStyleRule.parse("@counter-style " + counterStyleRule.name + " { " + buffer + " }");
  5421. counterStyleRule.__ends = i + 1;
  5422. // Add to parent's cssRules
  5423. if (counterStyleRule.__parentRule) {
  5424. counterStyleRule.__parentRule.cssRules.push(counterStyleRule);
  5425. } else {
  5426. topScope.cssRules.push(counterStyleRule);
  5427. }
  5428. }
  5429. // Restore currentScope to parent after closing this rule
  5430. if (counterStyleRule.__parentRule) {
  5431. currentScope = counterStyleRule.__parentRule;
  5432. }
  5433. styleRule = null;
  5434. buffer = "";
  5435. state = "before-selector";
  5436. break;
  5437. }
  5438. if (state === "propertyBlock") {
  5439. // Only process if name was set (valid)
  5440. if (propertyRule.__name) {
  5441. var parseSuccess = propertyRule.parse("@property " + propertyRule.__name + " { " + buffer + " }");
  5442. // Only add the rule if parse was successful (syntax, inherits, and initial-value validation passed)
  5443. if (parseSuccess) {
  5444. propertyRule.__ends = i + 1;
  5445. // Add to parent's cssRules
  5446. if (propertyRule.__parentRule) {
  5447. propertyRule.__parentRule.cssRules.push(propertyRule);
  5448. } else {
  5449. topScope.cssRules.push(propertyRule);
  5450. }
  5451. }
  5452. }
  5453. // Restore currentScope to parent after closing this rule
  5454. if (propertyRule.__parentRule) {
  5455. currentScope = propertyRule.__parentRule;
  5456. }
  5457. styleRule = null;
  5458. buffer = "";
  5459. state = "before-selector";
  5460. break;
  5461. }
  5462. switch (state) {
  5463. case "value":
  5464. styleRule.style.setProperty(name, buffer.trim(), priority, parseError);
  5465. priority = "";
  5466. /* falls through */
  5467. case "before-value":
  5468. case "before-name":
  5469. case "name":
  5470. styleRule.__ends = i + 1;
  5471. if (parentRule === styleRule) {
  5472. parentRule = ancestorRules.pop()
  5473. }
  5474. if (parentRule) {
  5475. styleRule.__parentRule = parentRule;
  5476. }
  5477. styleRule.__parentStyleSheet = styleSheet;
  5478. if (currentScope === styleRule) {
  5479. currentScope = parentRule || topScope;
  5480. }
  5481. if (styleRule.constructor.name === "CSSStyleRule" && !isValidSelectorText(styleRule.selectorText)) {
  5482. if (styleRule === nestedSelectorRule) {
  5483. nestedSelectorRule = null;
  5484. }
  5485. parseError('Invalid CSSStyleRule (selectorText = "' + styleRule.selectorText + '")', styleRule.parentRule !== null);
  5486. } else {
  5487. if (styleRule.parentRule) {
  5488. styleRule.parentRule.cssRules.push(styleRule);
  5489. } else {
  5490. currentScope.cssRules.push(styleRule);
  5491. }
  5492. }
  5493. buffer = "";
  5494. if (currentScope.constructor === CSSOM.CSSKeyframesRule) {
  5495. state = "keyframeRule-begin";
  5496. } else {
  5497. state = "before-selector";
  5498. }
  5499. if (styleRule.constructor.name === "CSSNestedDeclarations") {
  5500. if (currentScope !== topScope) {
  5501. // Only set nestedSelectorRule if currentScope is CSSStyleRule or CSSScopeRule
  5502. // Not for other grouping rules like @media/@supports
  5503. if (currentScope.constructor.name === "CSSStyleRule" || currentScope.constructor.name === "CSSScopeRule") {
  5504. nestedSelectorRule = currentScope;
  5505. }
  5506. }
  5507. styleRule = null;
  5508. } else {
  5509. // Update nestedSelectorRule when closing a CSSStyleRule
  5510. if (styleRule === nestedSelectorRule) {
  5511. var selector = styleRule.selectorText && styleRule.selectorText.trim();
  5512. // Check if this is proper nesting (&.class, &:pseudo) vs prepended & (& :is, & .class with space)
  5513. // Prepended & has pattern "& X" where X starts with : or .
  5514. var isPrependedAmpersand = selector && selector.match(prependedAmpersandRegExp);
  5515. // Check if parent is a grouping rule that can contain nested selectors
  5516. var isGroupingRule = currentScope && currentScope instanceof CSSOM.CSSGroupingRule;
  5517. if (!isPrependedAmpersand && isGroupingRule) {
  5518. // Proper nesting - set nestedSelectorRule to parent for more nested selectors
  5519. // But only if it's a CSSStyleRule or CSSScopeRule, not other grouping rules like @media
  5520. if (currentScope.constructor.name === "CSSStyleRule" || currentScope.constructor.name === "CSSScopeRule") {
  5521. nestedSelectorRule = currentScope;
  5522. }
  5523. // If currentScope is another type of grouping rule (like @media), keep nestedSelectorRule unchanged
  5524. } else {
  5525. // Prepended & or not nested in grouping rule - reset to prevent CSSNestedDeclarations
  5526. nestedSelectorRule = null;
  5527. }
  5528. } else if (nestedSelectorRule && currentScope instanceof CSSOM.CSSGroupingRule) {
  5529. // When closing a nested rule that's not the nestedSelectorRule itself,
  5530. // maintain nestedSelectorRule if we're still inside a grouping rule
  5531. // This ensures declarations after nested selectors inside @media/@supports etc. work correctly
  5532. }
  5533. styleRule = null;
  5534. break;
  5535. }
  5536. case "keyframeRule-begin":
  5537. case "before-selector":
  5538. case "selector":
  5539. // End of media/supports/document rule.
  5540. if (!parentRule) {
  5541. parseError("Unexpected }");
  5542. var hasPreviousStyleRule = currentScope.cssRules.length && currentScope.cssRules[currentScope.cssRules.length - 1].constructor.name === "CSSStyleRule";
  5543. if (hasPreviousStyleRule) {
  5544. i = ignoreBalancedBlock(i, token.slice(i), 1);
  5545. }
  5546. break;
  5547. }
  5548. // Find the actual parent rule by popping from ancestor stack
  5549. while (ancestorRules.length > 0) {
  5550. parentRule = ancestorRules.pop();
  5551. // Skip if we popped the current scope itself (happens because we push both rule and parent)
  5552. if (parentRule === currentScope) {
  5553. continue;
  5554. }
  5555. // Only process valid grouping rules
  5556. if (!(parentRule instanceof CSSOM.CSSGroupingRule && (parentRule.constructor.name !== 'CSSStyleRule' || parentRule.__parentRule))) {
  5557. continue;
  5558. }
  5559. // Determine if we're closing a special nested selector context
  5560. var isClosingNestedSelectorContext = nestedSelectorRule &&
  5561. (currentScope === nestedSelectorRule || nestedSelectorRule.__parentRule === currentScope);
  5562. if (isClosingNestedSelectorContext) {
  5563. // Closing the nestedSelectorRule or its direct container
  5564. if (nestedSelectorRule.parentRule) {
  5565. // Add nestedSelectorRule to its parent and update scope
  5566. prevScope = nestedSelectorRule;
  5567. currentScope = nestedSelectorRule.parentRule;
  5568. // Use object lookup instead of O(n) indexOf
  5569. var scopeId = getRuleId(prevScope);
  5570. if (!addedToCurrentScope[scopeId]) {
  5571. currentScope.cssRules.push(prevScope);
  5572. addedToCurrentScope[scopeId] = true;
  5573. }
  5574. nestedSelectorRule = currentScope;
  5575. // Stop here to preserve context for sibling selectors
  5576. break;
  5577. } else {
  5578. // Top-level CSSStyleRule with nested grouping rule
  5579. prevScope = currentScope;
  5580. var actualParent = ancestorRules.length > 0 ? ancestorRules[ancestorRules.length - 1] : nestedSelectorRule;
  5581. if (actualParent !== prevScope) {
  5582. actualParent.cssRules.push(prevScope);
  5583. }
  5584. currentScope = actualParent;
  5585. parentRule = actualParent;
  5586. break;
  5587. }
  5588. } else {
  5589. // Regular case: add currentScope to parentRule
  5590. prevScope = currentScope;
  5591. if (parentRule !== prevScope) {
  5592. parentRule.cssRules.push(prevScope);
  5593. }
  5594. break;
  5595. }
  5596. }
  5597. // If currentScope has a __parentRule and wasn't added yet, add it
  5598. if (ancestorRules.length === 0 && currentScope.__parentRule && currentScope.__parentRule.cssRules) {
  5599. // Use object lookup instead of O(n) findIndex
  5600. var parentId = getRuleId(currentScope);
  5601. if (!addedToParent[parentId]) {
  5602. currentScope.__parentRule.cssRules.push(currentScope);
  5603. addedToParent[parentId] = true;
  5604. }
  5605. }
  5606. // Only handle top-level rule closing if we processed all ancestors
  5607. if (ancestorRules.length === 0 && currentScope.parentRule == null) {
  5608. currentScope.__ends = i + 1;
  5609. // Use object lookup instead of O(n) findIndex
  5610. var topId = getRuleId(currentScope);
  5611. if (currentScope !== topScope && !addedToTopScope[topId]) {
  5612. topScope.cssRules.push(currentScope);
  5613. addedToTopScope[topId] = true;
  5614. }
  5615. currentScope = topScope;
  5616. if (nestedSelectorRule === parentRule) {
  5617. // Check if this selector is really starting inside another selector
  5618. var nestedSelectorTokenToCurrentSelectorToken = token.slice(nestedSelectorRule.__starts, i + 1);
  5619. var openingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(openBraceGlobalRegExp);
  5620. var closingBraceMatch = nestedSelectorTokenToCurrentSelectorToken.match(closeBraceGlobalRegExp);
  5621. var openingBraceLen = openingBraceMatch && openingBraceMatch.length;
  5622. var closingBraceLen = closingBraceMatch && closingBraceMatch.length;
  5623. if (openingBraceLen === closingBraceLen) {
  5624. // If the number of opening and closing braces are equal, we can assume that the new selector is starting outside the nestedSelectorRule
  5625. nestedSelectorRule.__ends = i + 1;
  5626. nestedSelectorRule = null;
  5627. parentRule = null;
  5628. }
  5629. } else {
  5630. parentRule = null;
  5631. }
  5632. } else {
  5633. currentScope = parentRule;
  5634. }
  5635. buffer = "";
  5636. state = "before-selector";
  5637. break;
  5638. }
  5639. break;
  5640. default:
  5641. switch (state) {
  5642. case "before-selector":
  5643. state = "selector";
  5644. if ((styleRule || scopeRule) && parentRule) {
  5645. // Assuming it's a declaration inside Nested Selector OR a Nested Declaration
  5646. // If Declaration inside Nested Selector let's keep the same styleRule
  5647. if (!isSelectorStartChar(character) && !isWhitespaceChar(character) && parentRule instanceof CSSOM.CSSGroupingRule) {
  5648. // parentRule.__parentRule = styleRule;
  5649. state = "before-name";
  5650. if (styleRule !== parentRule) {
  5651. styleRule = new CSSOM.CSSNestedDeclarations();
  5652. styleRule.__starts = i;
  5653. }
  5654. }
  5655. } else if (nestedSelectorRule && parentRule && parentRule instanceof CSSOM.CSSGroupingRule) {
  5656. if (isSelectorStartChar(character)) {
  5657. // If starting with a selector character, create CSSStyleRule instead of CSSNestedDeclarations
  5658. styleRule = new CSSOM.CSSStyleRule();
  5659. styleRule.__starts = i;
  5660. } else if (!isWhitespaceChar(character)) {
  5661. // Starting a declaration (not whitespace, not a selector)
  5662. state = "before-name";
  5663. // Check if we should create CSSNestedDeclarations
  5664. // This happens if: parent has cssRules OR nestedSelectorRule exists (indicating CSSStyleRule in hierarchy)
  5665. if (parentRule.cssRules.length || nestedSelectorRule) {
  5666. currentScope = parentRule;
  5667. // Only set nestedSelectorRule if parentRule is CSSStyleRule or CSSScopeRule
  5668. if (parentRule.constructor.name === "CSSStyleRule" || parentRule.constructor.name === "CSSScopeRule") {
  5669. nestedSelectorRule = parentRule;
  5670. }
  5671. styleRule = new CSSOM.CSSNestedDeclarations();
  5672. styleRule.__starts = i;
  5673. } else {
  5674. if (parentRule.constructor.name === "CSSStyleRule") {
  5675. styleRule = parentRule;
  5676. } else {
  5677. styleRule = new CSSOM.CSSStyleRule();
  5678. styleRule.__starts = i;
  5679. }
  5680. }
  5681. }
  5682. }
  5683. break;
  5684. case "before-name":
  5685. state = "name";
  5686. break;
  5687. case "before-value":
  5688. state = "value";
  5689. break;
  5690. case "importRule-begin":
  5691. state = "importRule";
  5692. break;
  5693. case "namespaceRule-begin":
  5694. state = "namespaceRule";
  5695. break;
  5696. }
  5697. buffer += character;
  5698. break;
  5699. }
  5700. // Auto-close all unclosed nested structures
  5701. // Check AFTER processing the character, at the ORIGINAL ending index
  5702. // Only add closing braces if CSS is incomplete (not at top scope)
  5703. if (i === initialEndingIndex && (currentScope !== topScope || ancestorRules.length > 0)) {
  5704. var needsClosing = ancestorRules.length;
  5705. if (currentScope !== topScope && ancestorRules.indexOf(currentScope) === -1) {
  5706. needsClosing += 1;
  5707. }
  5708. // Add closing braces for all unclosed structures
  5709. for (var closeIdx = 0; closeIdx < needsClosing; closeIdx++) {
  5710. token += "}";
  5711. endingIndex += 1;
  5712. }
  5713. }
  5714. }
  5715. if (buffer.trim() !== "") {
  5716. parseError("Unexpected end of input");
  5717. }
  5718. return styleSheet;
  5719. };
  5720. /**
  5721. * Produces a deep copy of stylesheet — the instance variables of stylesheet are copied recursively.
  5722. * @param {CSSStyleSheet|CSSOM.CSSStyleSheet} stylesheet
  5723. * @nosideeffects
  5724. * @return {CSSOM.CSSStyleSheet}
  5725. */
  5726. CSSOM.clone = function clone(stylesheet) {
  5727. var cloned = new CSSOM.CSSStyleSheet();
  5728. var rules = stylesheet.cssRules;
  5729. if (!rules) {
  5730. return cloned;
  5731. }
  5732. for (var i = 0, rulesLength = rules.length; i < rulesLength; i++) {
  5733. var rule = rules[i];
  5734. var ruleClone = cloned.cssRules[i] = new rule.constructor();
  5735. var style = rule.style;
  5736. if (style) {
  5737. var styleClone = ruleClone.style = new CSSOM.CSSStyleDeclaration();
  5738. for (var j = 0, styleLength = style.length; j < styleLength; j++) {
  5739. var name = styleClone[j] = style[j];
  5740. styleClone[name] = style[name];
  5741. styleClone._importants[name] = style.getPropertyPriority(name);
  5742. }
  5743. styleClone.length = style.length;
  5744. }
  5745. if (rule.hasOwnProperty('keyText')) {
  5746. ruleClone.keyText = rule.keyText;
  5747. }
  5748. if (rule.hasOwnProperty('selectorText')) {
  5749. ruleClone.selectorText = rule.selectorText;
  5750. }
  5751. if (rule.hasOwnProperty('mediaText')) {
  5752. ruleClone.mediaText = rule.mediaText;
  5753. }
  5754. if (rule.hasOwnProperty('supportsText')) {
  5755. ruleClone.supports = rule.supports;
  5756. }
  5757. if (rule.hasOwnProperty('conditionText')) {
  5758. ruleClone.conditionText = rule.conditionText;
  5759. }
  5760. if (rule.hasOwnProperty('layerName')) {
  5761. ruleClone.layerName = rule.layerName;
  5762. }
  5763. if (rule.hasOwnProperty('href')) {
  5764. ruleClone.href = rule.href;
  5765. }
  5766. if (rule.hasOwnProperty('name')) {
  5767. ruleClone.name = rule.name;
  5768. }
  5769. if (rule.hasOwnProperty('nameList')) {
  5770. ruleClone.nameList = rule.nameList;
  5771. }
  5772. if (rule.hasOwnProperty('cssRules')) {
  5773. ruleClone.cssRules = clone(rule).cssRules;
  5774. }
  5775. }
  5776. return cloned;
  5777. };