syntax.js 496 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468746974707471747274737474747574767477747874797480748174827483748474857486748774887489749074917492749374947495749674977498749975007501750275037504750575067507750875097510751175127513751475157516751775187519752075217522752375247525752675277528752975307531753275337534753575367537753875397540754175427543754475457546754775487549755075517552755375547555755675577558755975607561756275637564756575667567756875697570757175727573757475757576757775787579758075817582758375847585758675877588758975907591759275937594759575967597759875997600760176027603760476057606760776087609761076117612761376147615761676177618761976207621762276237624762576267627762876297630763176327633763476357636763776387639764076417642764376447645764676477648764976507651765276537654765576567657765876597660766176627663766476657666766776687669767076717672767376747675767676777678767976807681768276837684768576867687768876897690769176927693769476957696769776987699770077017702770377047705770677077708770977107711771277137714771577167717771877197720772177227723772477257726772777287729773077317732773377347735773677377738773977407741774277437744774577467747774877497750775177527753775477557756775777587759776077617762776377647765776677677768776977707771777277737774777577767777777877797780778177827783778477857786778777887789779077917792779377947795779677977798779978007801780278037804780578067807780878097810781178127813781478157816781778187819782078217822782378247825782678277828782978307831783278337834783578367837783878397840784178427843784478457846784778487849785078517852785378547855785678577858785978607861786278637864786578667867786878697870787178727873787478757876787778787879788078817882788378847885788678877888788978907891789278937894789578967897789878997900790179027903790479057906790779087909791079117912791379147915791679177918791979207921792279237924792579267927792879297930793179327933793479357936793779387939794079417942794379447945794679477948794979507951795279537954795579567957795879597960796179627963796479657966796779687969797079717972797379747975797679777978797979807981798279837984798579867987798879897990799179927993799479957996799779987999800080018002800380048005800680078008800980108011801280138014801580168017801880198020802180228023802480258026802780288029803080318032803380348035803680378038803980408041804280438044804580468047804880498050805180528053805480558056805780588059806080618062806380648065806680678068806980708071807280738074807580768077807880798080808180828083808480858086808780888089809080918092809380948095809680978098809981008101810281038104810581068107810881098110811181128113811481158116811781188119812081218122812381248125812681278128812981308131813281338134813581368137813881398140814181428143814481458146814781488149815081518152815381548155815681578158815981608161816281638164816581668167816881698170817181728173817481758176817781788179818081818182818381848185818681878188818981908191819281938194819581968197819881998200820182028203820482058206820782088209821082118212821382148215821682178218821982208221822282238224822582268227822882298230823182328233823482358236823782388239824082418242824382448245824682478248824982508251825282538254825582568257825882598260826182628263826482658266826782688269827082718272827382748275827682778278827982808281828282838284828582868287828882898290829182928293829482958296829782988299830083018302830383048305830683078308830983108311831283138314831583168317831883198320832183228323832483258326832783288329833083318332833383348335833683378338833983408341834283438344834583468347834883498350835183528353835483558356835783588359836083618362836383648365836683678368836983708371837283738374837583768377837883798380838183828383838483858386838783888389839083918392839383948395839683978398839984008401840284038404840584068407840884098410841184128413841484158416841784188419842084218422842384248425842684278428842984308431843284338434843584368437843884398440844184428443844484458446844784488449845084518452845384548455845684578458845984608461846284638464846584668467846884698470847184728473847484758476847784788479848084818482848384848485848684878488848984908491849284938494849584968497849884998500850185028503850485058506850785088509851085118512851385148515851685178518851985208521852285238524852585268527852885298530853185328533853485358536853785388539854085418542854385448545854685478548854985508551855285538554855585568557855885598560856185628563856485658566856785688569857085718572857385748575857685778578857985808581858285838584858585868587858885898590859185928593859485958596859785988599860086018602860386048605860686078608860986108611861286138614861586168617861886198620862186228623862486258626862786288629863086318632863386348635863686378638863986408641864286438644864586468647864886498650865186528653865486558656865786588659866086618662866386648665866686678668866986708671867286738674867586768677867886798680868186828683868486858686868786888689869086918692869386948695869686978698869987008701870287038704870587068707870887098710871187128713871487158716871787188719872087218722872387248725872687278728872987308731873287338734873587368737873887398740874187428743874487458746874787488749875087518752875387548755875687578758875987608761876287638764876587668767876887698770877187728773877487758776877787788779878087818782878387848785878687878788878987908791879287938794879587968797879887998800880188028803880488058806880788088809881088118812881388148815881688178818881988208821882288238824882588268827882888298830883188328833883488358836883788388839884088418842884388448845884688478848884988508851885288538854885588568857885888598860886188628863886488658866886788688869887088718872887388748875887688778878887988808881888288838884888588868887888888898890889188928893889488958896889788988899890089018902890389048905890689078908890989108911891289138914891589168917891889198920892189228923892489258926892789288929893089318932893389348935893689378938893989408941894289438944894589468947894889498950895189528953895489558956895789588959896089618962896389648965896689678968896989708971897289738974897589768977897889798980898189828983898489858986898789888989899089918992899389948995899689978998899990009001900290039004900590069007900890099010901190129013901490159016901790189019902090219022902390249025902690279028902990309031903290339034903590369037903890399040904190429043904490459046904790489049905090519052905390549055905690579058905990609061906290639064906590669067906890699070907190729073907490759076907790789079908090819082908390849085908690879088908990909091909290939094909590969097909890999100910191029103910491059106910791089109911091119112911391149115911691179118911991209121912291239124912591269127912891299130913191329133913491359136913791389139914091419142914391449145914691479148914991509151915291539154915591569157915891599160916191629163916491659166916791689169917091719172917391749175917691779178917991809181918291839184918591869187918891899190919191929193919491959196919791989199920092019202920392049205920692079208920992109211921292139214921592169217921892199220922192229223922492259226922792289229923092319232923392349235923692379238923992409241924292439244924592469247924892499250925192529253925492559256925792589259926092619262926392649265926692679268926992709271927292739274927592769277927892799280928192829283928492859286928792889289929092919292929392949295929692979298929993009301930293039304930593069307930893099310931193129313931493159316931793189319932093219322932393249325932693279328932993309331933293339334933593369337933893399340934193429343934493459346934793489349935093519352935393549355935693579358935993609361936293639364936593669367936893699370937193729373937493759376937793789379938093819382938393849385938693879388938993909391939293939394939593969397939893999400940194029403940494059406940794089409941094119412941394149415941694179418941994209421942294239424942594269427942894299430943194329433943494359436943794389439944094419442944394449445944694479448944994509451945294539454945594569457945894599460946194629463946494659466946794689469947094719472947394749475947694779478947994809481948294839484948594869487948894899490949194929493949494959496949794989499950095019502950395049505950695079508950995109511951295139514951595169517951895199520952195229523952495259526952795289529953095319532953395349535953695379538953995409541954295439544954595469547954895499550955195529553955495559556955795589559956095619562956395649565956695679568956995709571957295739574957595769577957895799580958195829583958495859586958795889589959095919592959395949595959695979598959996009601960296039604960596069607960896099610961196129613961496159616961796189619962096219622962396249625962696279628962996309631963296339634963596369637963896399640964196429643964496459646964796489649965096519652965396549655965696579658965996609661966296639664966596669667966896699670967196729673967496759676967796789679968096819682968396849685968696879688968996909691969296939694969596969697969896999700970197029703970497059706970797089709971097119712971397149715971697179718971997209721972297239724972597269727972897299730973197329733973497359736973797389739974097419742974397449745974697479748974997509751975297539754975597569757975897599760976197629763976497659766976797689769977097719772977397749775977697779778977997809781978297839784978597869787978897899790979197929793979497959796979797989799980098019802980398049805980698079808980998109811981298139814981598169817981898199820982198229823982498259826982798289829983098319832983398349835983698379838983998409841984298439844984598469847984898499850985198529853985498559856985798589859986098619862986398649865986698679868986998709871987298739874987598769877987898799880988198829883988498859886988798889889989098919892989398949895989698979898989999009901990299039904990599069907990899099910991199129913991499159916991799189919992099219922992399249925992699279928992999309931993299339934993599369937993899399940994199429943994499459946994799489949995099519952995399549955995699579958995999609961996299639964996599669967996899699970997199729973997499759976997799789979998099819982998399849985998699879988998999909991999299939994999599969997999899991000010001100021000310004100051000610007100081000910010100111001210013100141001510016100171001810019100201002110022100231002410025100261002710028100291003010031100321003310034100351003610037100381003910040100411004210043100441004510046100471004810049100501005110052100531005410055100561005710058100591006010061100621006310064100651006610067100681006910070100711007210073100741007510076100771007810079100801008110082100831008410085100861008710088100891009010091100921009310094100951009610097100981009910100101011010210103101041010510106101071010810109101101011110112101131011410115101161011710118101191012010121101221012310124101251012610127101281012910130101311013210133101341013510136101371013810139101401014110142101431014410145101461014710148101491015010151101521015310154101551015610157101581015910160101611016210163101641016510166101671016810169101701017110172101731017410175101761017710178101791018010181101821018310184101851018610187101881018910190101911019210193101941019510196101971019810199102001020110202102031020410205102061020710208102091021010211102121021310214102151021610217102181021910220102211022210223102241022510226102271022810229102301023110232102331023410235102361023710238102391024010241102421024310244102451024610247102481024910250102511025210253102541025510256102571025810259102601026110262102631026410265102661026710268102691027010271102721027310274102751027610277102781027910280102811028210283102841028510286102871028810289102901029110292102931029410295102961029710298102991030010301103021030310304103051030610307103081030910310103111031210313103141031510316103171031810319103201032110322103231032410325103261032710328103291033010331103321033310334103351033610337103381033910340103411034210343103441034510346103471034810349103501035110352103531035410355103561035710358103591036010361103621036310364103651036610367103681036910370103711037210373103741037510376103771037810379103801038110382103831038410385103861038710388103891039010391103921039310394103951039610397103981039910400104011040210403104041040510406104071040810409104101041110412104131041410415104161041710418104191042010421104221042310424104251042610427104281042910430104311043210433104341043510436104371043810439104401044110442104431044410445104461044710448104491045010451104521045310454104551045610457104581045910460104611046210463104641046510466104671046810469104701047110472104731047410475104761047710478104791048010481104821048310484104851048610487104881048910490104911049210493104941049510496104971049810499105001050110502105031050410505105061050710508105091051010511105121051310514105151051610517105181051910520105211052210523105241052510526105271052810529105301053110532105331053410535105361053710538105391054010541105421054310544105451054610547105481054910550105511055210553105541055510556105571055810559105601056110562105631056410565105661056710568105691057010571105721057310574105751057610577105781057910580105811058210583105841058510586105871058810589105901059110592105931059410595105961059710598105991060010601106021060310604106051060610607106081060910610106111061210613106141061510616106171061810619106201062110622106231062410625106261062710628106291063010631106321063310634106351063610637106381063910640106411064210643106441064510646106471064810649106501065110652106531065410655106561065710658106591066010661106621066310664106651066610667106681066910670106711067210673106741067510676106771067810679106801068110682106831068410685106861068710688106891069010691106921069310694106951069610697106981069910700107011070210703107041070510706107071070810709107101071110712107131071410715107161071710718107191072010721107221072310724107251072610727107281072910730107311073210733107341073510736107371073810739107401074110742107431074410745107461074710748107491075010751107521075310754107551075610757107581075910760107611076210763107641076510766107671076810769107701077110772107731077410775107761077710778107791078010781107821078310784107851078610787107881078910790107911079210793107941079510796107971079810799108001080110802108031080410805108061080710808108091081010811108121081310814108151081610817108181081910820108211082210823108241082510826108271082810829108301083110832108331083410835108361083710838108391084010841108421084310844108451084610847108481084910850108511085210853108541085510856108571085810859108601086110862108631086410865108661086710868108691087010871108721087310874108751087610877108781087910880108811088210883108841088510886108871088810889108901089110892108931089410895108961089710898108991090010901109021090310904109051090610907109081090910910109111091210913109141091510916109171091810919109201092110922109231092410925109261092710928109291093010931109321093310934109351093610937109381093910940109411094210943109441094510946109471094810949109501095110952109531095410955109561095710958109591096010961109621096310964109651096610967109681096910970109711097210973109741097510976109771097810979109801098110982109831098410985109861098710988109891099010991109921099310994109951099610997109981099911000110011100211003110041100511006110071100811009110101101111012110131101411015110161101711018110191102011021110221102311024110251102611027110281102911030110311103211033110341103511036110371103811039110401104111042110431104411045110461104711048110491105011051110521105311054110551105611057110581105911060110611106211063110641106511066110671106811069110701107111072110731107411075110761107711078110791108011081110821108311084110851108611087110881108911090110911109211093110941109511096110971109811099111001110111102111031110411105111061110711108111091111011111111121111311114111151111611117111181111911120111211112211123111241112511126111271112811129111301113111132111331113411135111361113711138111391114011141111421114311144111451114611147111481114911150111511115211153111541115511156111571115811159111601116111162111631116411165111661116711168111691117011171111721117311174111751117611177111781117911180111811118211183111841118511186111871118811189111901119111192111931119411195111961119711198111991120011201112021120311204112051120611207112081120911210112111121211213112141121511216112171121811219112201122111222112231122411225112261122711228112291123011231112321123311234112351123611237112381123911240112411124211243112441124511246112471124811249112501125111252112531125411255112561125711258112591126011261112621126311264112651126611267112681126911270112711127211273112741127511276112771127811279112801128111282112831128411285112861128711288112891129011291112921129311294112951129611297112981129911300113011130211303113041130511306113071130811309113101131111312113131131411315113161131711318113191132011321113221132311324113251132611327113281132911330113311133211333113341133511336113371133811339113401134111342113431134411345113461134711348113491135011351113521135311354113551135611357113581135911360113611136211363113641136511366113671136811369113701137111372113731137411375113761137711378113791138011381113821138311384113851138611387113881138911390113911139211393113941139511396113971139811399114001140111402114031140411405114061140711408114091141011411114121141311414114151141611417114181141911420114211142211423114241142511426114271142811429114301143111432114331143411435114361143711438114391144011441114421144311444114451144611447114481144911450114511145211453114541145511456114571145811459114601146111462114631146411465114661146711468114691147011471114721147311474114751147611477114781147911480114811148211483114841148511486114871148811489114901149111492114931149411495114961149711498114991150011501115021150311504115051150611507115081150911510115111151211513115141151511516115171151811519115201152111522115231152411525115261152711528115291153011531115321153311534115351153611537115381153911540115411154211543115441154511546115471154811549115501155111552115531155411555115561155711558115591156011561115621156311564115651156611567115681156911570115711157211573115741157511576115771157811579115801158111582115831158411585115861158711588115891159011591115921159311594115951159611597115981159911600116011160211603116041160511606116071160811609116101161111612116131161411615116161161711618116191162011621116221162311624116251162611627116281162911630116311163211633116341163511636116371163811639116401164111642116431164411645116461164711648116491165011651116521165311654116551165611657116581165911660116611166211663116641166511666116671166811669116701167111672116731167411675116761167711678116791168011681116821168311684116851168611687116881168911690116911169211693116941169511696116971169811699117001170111702117031170411705117061170711708117091171011711117121171311714117151171611717117181171911720117211172211723117241172511726117271172811729117301173111732117331173411735117361173711738117391174011741117421174311744117451174611747117481174911750117511175211753117541175511756117571175811759117601176111762117631176411765117661176711768117691177011771117721177311774117751177611777117781177911780117811178211783117841178511786117871178811789117901179111792117931179411795117961179711798117991180011801118021180311804118051180611807118081180911810118111181211813118141181511816118171181811819118201182111822118231182411825118261182711828118291183011831118321183311834118351183611837118381183911840118411184211843118441184511846118471184811849118501185111852118531185411855118561185711858118591186011861118621186311864118651186611867118681186911870118711187211873118741187511876118771187811879118801188111882118831188411885118861188711888118891189011891118921189311894118951189611897118981189911900119011190211903119041190511906119071190811909119101191111912119131191411915119161191711918119191192011921119221192311924119251192611927119281192911930119311193211933119341193511936119371193811939119401194111942119431194411945119461194711948119491195011951119521195311954119551195611957119581195911960119611196211963119641196511966119671196811969119701197111972119731197411975119761197711978119791198011981119821198311984119851198611987119881198911990119911199211993119941199511996119971199811999120001200112002120031200412005120061200712008120091201012011120121201312014120151201612017120181201912020120211202212023120241202512026120271202812029120301203112032120331203412035120361203712038120391204012041120421204312044120451204612047120481204912050120511205212053120541205512056120571205812059120601206112062120631206412065120661206712068120691207012071120721207312074120751207612077120781207912080120811208212083120841208512086120871208812089120901209112092120931209412095120961209712098120991210012101121021210312104121051210612107121081210912110121111211212113121141211512116121171211812119121201212112122121231212412125121261212712128121291213012131121321213312134121351213612137121381213912140121411214212143121441214512146121471214812149121501215112152121531215412155121561215712158121591216012161121621216312164121651216612167121681216912170121711217212173121741217512176121771217812179121801218112182121831218412185121861218712188121891219012191121921219312194121951219612197121981219912200122011220212203122041220512206122071220812209122101221112212122131221412215122161221712218122191222012221122221222312224122251222612227122281222912230122311223212233122341223512236122371223812239122401224112242122431224412245122461224712248122491225012251122521225312254122551225612257122581225912260122611226212263122641226512266122671226812269122701227112272122731227412275122761227712278122791228012281122821228312284122851228612287122881228912290122911229212293122941229512296122971229812299123001230112302123031230412305123061230712308123091231012311123121231312314123151231612317123181231912320123211232212323123241232512326123271232812329123301233112332123331233412335123361233712338123391234012341123421234312344123451234612347123481234912350123511235212353123541235512356123571235812359123601236112362123631236412365123661236712368123691237012371123721237312374123751237612377123781237912380123811238212383123841238512386123871238812389123901239112392123931239412395123961239712398123991240012401124021240312404124051240612407124081240912410124111241212413124141241512416124171241812419124201242112422124231242412425124261242712428124291243012431124321243312434124351243612437124381243912440124411244212443124441244512446124471244812449124501245112452124531245412455124561245712458124591246012461124621246312464124651246612467124681246912470124711247212473124741247512476124771247812479124801248112482124831248412485124861248712488124891249012491124921249312494124951249612497124981249912500125011250212503125041250512506125071250812509125101251112512125131251412515125161251712518125191252012521125221252312524125251252612527125281252912530125311253212533125341253512536125371253812539125401254112542125431254412545125461254712548125491255012551125521255312554125551255612557125581255912560125611256212563125641256512566125671256812569125701257112572125731257412575125761257712578125791258012581125821258312584125851258612587125881258912590125911259212593125941259512596125971259812599126001260112602126031260412605126061260712608126091261012611126121261312614126151261612617126181261912620126211262212623126241262512626126271262812629126301263112632126331263412635126361263712638126391264012641126421264312644126451264612647126481264912650126511265212653126541265512656126571265812659126601266112662126631266412665126661266712668126691267012671126721267312674126751267612677126781267912680126811268212683126841268512686126871268812689126901269112692126931269412695126961269712698126991270012701127021270312704127051270612707127081270912710127111271212713127141271512716127171271812719127201272112722127231272412725127261272712728127291273012731127321273312734127351273612737127381273912740127411274212743127441274512746127471274812749127501275112752127531275412755127561275712758127591276012761127621276312764127651276612767127681276912770127711277212773127741277512776127771277812779127801278112782127831278412785127861278712788127891279012791127921279312794127951279612797127981279912800128011280212803128041280512806128071280812809128101281112812128131281412815128161281712818128191282012821128221282312824128251282612827128281282912830128311283212833128341283512836128371283812839128401284112842128431284412845128461284712848128491285012851128521285312854128551285612857128581285912860128611286212863128641286512866128671286812869128701287112872128731287412875128761287712878128791288012881128821288312884128851288612887128881288912890128911289212893128941289512896128971289812899129001290112902129031290412905129061290712908129091291012911129121291312914129151291612917129181291912920129211292212923129241292512926129271292812929129301293112932129331293412935129361293712938129391294012941129421294312944129451294612947129481294912950129511295212953129541295512956129571295812959129601296112962129631296412965129661296712968129691297012971129721297312974129751297612977129781297912980129811298212983129841298512986129871298812989129901299112992129931299412995129961299712998129991300013001130021300313004130051300613007130081300913010130111301213013130141301513016130171301813019130201302113022130231302413025130261302713028130291303013031130321303313034130351303613037130381303913040130411304213043130441304513046130471304813049130501305113052130531305413055130561305713058130591306013061130621306313064130651306613067130681306913070130711307213073130741307513076130771307813079130801308113082130831308413085130861308713088130891309013091130921309313094130951309613097130981309913100131011310213103131041310513106131071310813109131101311113112131131311413115131161311713118131191312013121131221312313124131251312613127131281312913130131311313213133131341313513136131371313813139131401314113142131431314413145131461314713148131491315013151131521315313154131551315613157131581315913160131611316213163131641316513166131671316813169131701317113172131731317413175131761317713178131791318013181131821318313184131851318613187131881318913190131911319213193131941319513196131971319813199132001320113202132031320413205132061320713208132091321013211132121321313214132151321613217132181321913220132211322213223132241322513226132271322813229132301323113232132331323413235132361323713238132391324013241132421324313244132451324613247132481324913250132511325213253132541325513256132571325813259132601326113262132631326413265132661326713268132691327013271132721327313274132751327613277132781327913280132811328213283132841328513286132871328813289132901329113292132931329413295132961329713298132991330013301133021330313304133051330613307133081330913310133111331213313133141331513316133171331813319133201332113322133231332413325133261332713328133291333013331133321333313334133351333613337133381333913340133411334213343133441334513346133471334813349133501335113352133531335413355133561335713358133591336013361133621336313364133651336613367133681336913370133711337213373133741337513376133771337813379133801338113382133831338413385133861338713388133891339013391133921339313394133951339613397133981339913400134011340213403134041340513406134071340813409134101341113412134131341413415134161341713418134191342013421134221342313424134251342613427134281342913430134311343213433134341343513436134371343813439134401344113442134431344413445134461344713448134491345013451134521345313454134551345613457134581345913460134611346213463134641346513466134671346813469134701347113472134731347413475134761347713478134791348013481134821348313484134851348613487134881348913490134911349213493134941349513496134971349813499135001350113502135031350413505135061350713508
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Raj Aryan (based on SWC parser by Alexander Akait)
  4. */
  5. "use strict";
  6. const {
  7. CSS_TYPE,
  8. HTML_TYPE,
  9. JAVASCRIPT_TYPE
  10. } = require("../ModuleSourceTypeConstants");
  11. const GenericSourceProcessor = require("../util/SourceProcessor");
  12. const { deferredWrite } = GenericSourceProcessor;
  13. /** @import { CssEnvironment, CssProcessOptions, CssTransformOptions } from "../css/syntax" */
  14. /** @import { OutputHtmlOptions } from "../../declarations/WebpackOptions" */
  15. /**
  16. * Renders one nested body this document embeds — an inline `<style>`, every
  17. * `style=""` (handed over as a whole stylesheet), a `<script>` holding JSON or
  18. * JavaScript, an `<svg>` subtree, and the document an `<iframe srcdoc>` holds. Returning the input unchanged declines
  19. * it. Synchronous: it is called from inside the print walk, so a caller with an
  20. * async minifier collects the bodies in a walk-only pass first and looks the
  21. * results up here.
  22. *
  23. * What comes back is spliced in as-is, so an `<svg>` renderer must return
  24. * markup — everything else is literal text. A `<style>` / `<script>` body may
  25. * therefore carry its own `sourceMappingURL`, which is the only way a map
  26. * reaches inline content; nothing else here can hold one.
  27. * @typedef {(source: string, info: { type: string, hostType: string, as?: string }) => string | undefined} EmbeddedSourceRenderer
  28. */
  29. /**
  30. * One embedded source recorded for a caller that can only answer
  31. * asynchronously, and the text to print once it has.
  32. * @typedef {import("../util/dataURL").DeferredEmbeddedSource} DeferredEmbeddedSource
  33. */
  34. // Neither a JSON `<script>` body nor an `<svg>` subtree is a module source
  35. // type, so neither has a constant in `ModuleSourceTypeConstants` — they name
  36. // themselves here.
  37. const JSON_TYPE = "json";
  38. const SVG_TYPE = "svg";
  39. // A `style=""` holds a block's contents, not a stylesheet. It is still CSS, so
  40. // it is offered as CSS — with `as` saying which production it is, the same
  41. // vocabulary `module.parser.css.as` uses.
  42. const BLOCK_CONTENTS = "block-contents";
  43. const {
  44. askEmbeddedRenderer,
  45. collectEmbeddedDiagnostics,
  46. embeddedText
  47. } = require("../util/dataURL");
  48. // Every language `renderEmbeddedSource` is offered from a document, in the
  49. // order the offer sites below reach them. A new site adds its language here, so
  50. // a caller that has to say up front which it handles reads one list.
  51. const EMBEDDED_LANGUAGES = [
  52. CSS_TYPE,
  53. JAVASCRIPT_TYPE,
  54. JSON_TYPE,
  55. SVG_TYPE,
  56. HTML_TYPE
  57. ];
  58. const {
  59. ADDRESS_DIV_P,
  60. APPLET_MARQUEE_OBJECT,
  61. BLOCK_END,
  62. BLOCK_LEVEL_ELEMENTS,
  63. BLOCK_START,
  64. BODY_HTML_BR,
  65. BODY_START_KEPT_BEFORE,
  66. BOOLEAN_ATTRIBUTES,
  67. CAPTION_IGNORED_ENDS,
  68. CAPTION_TABLE_STARTS,
  69. CELL_IGNORED_ENDS,
  70. CLEAR_TABLE,
  71. CLEAR_TABLE_BODY,
  72. CLEAR_TABLE_ROW,
  73. COMMA_LIST_ATTRIBUTES,
  74. DOM_TOKEN_LIST_ATTRIBUTES,
  75. EMPTY_ELEMENT_KEPT,
  76. EMPTY_METADATA_ELEMENTS,
  77. EMPTY_REMOVABLE_ATTRIBUTES,
  78. ENUMERATED_ATTRIBUTE_NAMES,
  79. ENUMERATED_KEYWORDS,
  80. FONT_BREAKOUT_ATTRS,
  81. FOREIGN_ATTR_NS,
  82. FOREIGN_BREAKOUT,
  83. FORMATTING,
  84. FOSTER_KEPT,
  85. HEADING,
  86. HEAD_BODY_HTML_BR,
  87. HEAD_ELEMENTS,
  88. HEAD_VOID_ELEMENTS,
  89. HTML_SCOPE,
  90. IGNORED_BODY_TABLE_STARTS,
  91. IMPLIED,
  92. IMPLIED_THOROUGH,
  93. INTEGER_ATTRIBUTES,
  94. IN_HEAD_NOSCRIPT_PASSTHROUGH,
  95. IN_TABLE_IGNORED_ENDS,
  96. JAVASCRIPT_SCRIPT_TYPES,
  97. JSON_SCRIPT_TYPES,
  98. LEADING_NEWLINE_ELEMENTS,
  99. LITERAL_TEXT_PARENTS,
  100. MATHML_SPECIAL,
  101. MATHML_TEXT_INTEGRATION,
  102. NOFRAMES_STYLE_NOSCRIPT,
  103. NO_DECODE_TEXT,
  104. OPTIONAL_END_TAG_AT_END,
  105. OPTIONAL_END_TAG_FOLLOWERS,
  106. OPTIONAL_END_TAG_UNLESS_TRAILING_NODE,
  107. PARAM_SOURCE_TRACK,
  108. P_ENDS_ON_PARENT_END_TAG,
  109. QUIRKY_EXACT,
  110. QUIRKY_PREFIXES,
  111. RAW_TEXT_ELEMENTS,
  112. REDUNDANT_DEFAULT_ATTRIBUTES,
  113. REDUNDANT_TYPE_ATTRIBUTES,
  114. RESERVED_ELEMENT_NAMES,
  115. REWRITABLE_ATTRIBUTES,
  116. ROW_IGNORED_ENDS,
  117. ROW_TRIGGER_STARTS,
  118. SCOPE_ENDED_BY,
  119. SHADOW_HOSTS,
  120. SHELL_ELEMENTS,
  121. SIGNED_INTEGER_ATTRIBUTES,
  122. SPECIAL,
  123. SRCSET_ATTRIBUTES,
  124. STYLE_SCRIPT_TEMPLATE,
  125. SVG_ATTR_ADJUST,
  126. SVG_SPECIAL,
  127. SVG_TAG_ADJUST,
  128. TABLE_CONTEXT,
  129. TABLE_SCOPE_STOP,
  130. TBODY_GROUP,
  131. TBODY_IGNORED_ENDS,
  132. TBODY_TRIGGER_STARTS,
  133. TD_TH,
  134. TD_TH_TR,
  135. TEMPLATE_START_TAG_MODES: TEMPLATE_START_TAG_MODE_NAMES,
  136. TOKEN_LIST_ATTRIBUTES,
  137. TRANSPARENT_IMPLIED_ELEMENTS,
  138. URL_ATTRIBUTES,
  139. VOID,
  140. VOID_FORMATTING
  141. } = require("./data");
  142. // cspell:ignore apos notpre noncharacter noncharacters DFFF FFFE CCLS ALNUM
  143. // #region html entities
  144. // The contents of this region are auto-generated by
  145. // `tooling/generate-html-entities.js` from `tooling/html-entities.json`.
  146. // Do not edit by hand — re-run the generator (via `yarn fix:special`) to refresh.
  147. //
  148. // WHATWG named character references. Keys are entity names WITHOUT the
  149. // leading `&` (some end with `;`, others omit it for legacy entities that
  150. // match without a closing semicolon). Values are the decoded character
  151. // strings (1–2 UTF-16 code units).
  152. // Built on a null prototype so bracket lookups (`HTML_ENTITIES[name]`)
  153. // can't be poisoned by inherited `Object.prototype` keys like `toString`,
  154. // `constructor`, or `__proto__` — without this, `&toString;` would falsely
  155. // look like a matched named character reference.
  156. // prettier-ignore
  157. // cspell:disable-next-line
  158. const HTML_ENTITIES = /** @type {Readonly<Record<string, string>>} */ (Object.freeze(Object.assign(Object.create(null), {"AElig":"Æ","AElig;":"Æ","AMP":"&","AMP;":"&","Aacute":"Á","Aacute;":"Á","Abreve;":"Ă","Acirc":"Â","Acirc;":"Â","Acy;":"А","Afr;":"𝔄","Agrave":"À","Agrave;":"À","Alpha;":"Α","Amacr;":"Ā","And;":"⩓","Aogon;":"Ą","Aopf;":"𝔸","ApplyFunction;":"⁡","Aring":"Å","Aring;":"Å","Ascr;":"𝒜","Assign;":"≔","Atilde":"Ã","Atilde;":"Ã","Auml":"Ä","Auml;":"Ä","Backslash;":"∖","Barv;":"⫧","Barwed;":"⌆","Bcy;":"Б","Because;":"∵","Bernoullis;":"ℬ","Beta;":"Β","Bfr;":"𝔅","Bopf;":"𝔹","Breve;":"˘","Bscr;":"ℬ","Bumpeq;":"≎","CHcy;":"Ч","COPY":"©","COPY;":"©","Cacute;":"Ć","Cap;":"⋒","CapitalDifferentialD;":"ⅅ","Cayleys;":"ℭ","Ccaron;":"Č","Ccedil":"Ç","Ccedil;":"Ç","Ccirc;":"Ĉ","Cconint;":"∰","Cdot;":"Ċ","Cedilla;":"¸","CenterDot;":"·","Cfr;":"ℭ","Chi;":"Χ","CircleDot;":"⊙","CircleMinus;":"⊖","CirclePlus;":"⊕","CircleTimes;":"⊗","ClockwiseContourIntegral;":"∲","CloseCurlyDoubleQuote;":"”","CloseCurlyQuote;":"’","Colon;":"∷","Colone;":"⩴","Congruent;":"≡","Conint;":"∯","ContourIntegral;":"∮","Copf;":"ℂ","Coproduct;":"∐","CounterClockwiseContourIntegral;":"∳","Cross;":"⨯","Cscr;":"𝒞","Cup;":"⋓","CupCap;":"≍","DD;":"ⅅ","DDotrahd;":"⤑","DJcy;":"Ђ","DScy;":"Ѕ","DZcy;":"Џ","Dagger;":"‡","Darr;":"↡","Dashv;":"⫤","Dcaron;":"Ď","Dcy;":"Д","Del;":"∇","Delta;":"Δ","Dfr;":"𝔇","DiacriticalAcute;":"´","DiacriticalDot;":"˙","DiacriticalDoubleAcute;":"˝","DiacriticalGrave;":"`","DiacriticalTilde;":"˜","Diamond;":"⋄","DifferentialD;":"ⅆ","Dopf;":"𝔻","Dot;":"¨","DotDot;":"⃜","DotEqual;":"≐","DoubleContourIntegral;":"∯","DoubleDot;":"¨","DoubleDownArrow;":"⇓","DoubleLeftArrow;":"⇐","DoubleLeftRightArrow;":"⇔","DoubleLeftTee;":"⫤","DoubleLongLeftArrow;":"⟸","DoubleLongLeftRightArrow;":"⟺","DoubleLongRightArrow;":"⟹","DoubleRightArrow;":"⇒","DoubleRightTee;":"⊨","DoubleUpArrow;":"⇑","DoubleUpDownArrow;":"⇕","DoubleVerticalBar;":"∥","DownArrow;":"↓","DownArrowBar;":"⤓","DownArrowUpArrow;":"⇵","DownBreve;":"̑","DownLeftRightVector;":"⥐","DownLeftTeeVector;":"⥞","DownLeftVector;":"↽","DownLeftVectorBar;":"⥖","DownRightTeeVector;":"⥟","DownRightVector;":"⇁","DownRightVectorBar;":"⥗","DownTee;":"⊤","DownTeeArrow;":"↧","Downarrow;":"⇓","Dscr;":"𝒟","Dstrok;":"Đ","ENG;":"Ŋ","ETH":"Ð","ETH;":"Ð","Eacute":"É","Eacute;":"É","Ecaron;":"Ě","Ecirc":"Ê","Ecirc;":"Ê","Ecy;":"Э","Edot;":"Ė","Efr;":"𝔈","Egrave":"È","Egrave;":"È","Element;":"∈","Emacr;":"Ē","EmptySmallSquare;":"◻","EmptyVerySmallSquare;":"▫","Eogon;":"Ę","Eopf;":"𝔼","Epsilon;":"Ε","Equal;":"⩵","EqualTilde;":"≂","Equilibrium;":"⇌","Escr;":"ℰ","Esim;":"⩳","Eta;":"Η","Euml":"Ë","Euml;":"Ë","Exists;":"∃","ExponentialE;":"ⅇ","Fcy;":"Ф","Ffr;":"𝔉","FilledSmallSquare;":"◼","FilledVerySmallSquare;":"▪","Fopf;":"𝔽","ForAll;":"∀","Fouriertrf;":"ℱ","Fscr;":"ℱ","GJcy;":"Ѓ","GT":">","GT;":">","Gamma;":"Γ","Gammad;":"Ϝ","Gbreve;":"Ğ","Gcedil;":"Ģ","Gcirc;":"Ĝ","Gcy;":"Г","Gdot;":"Ġ","Gfr;":"𝔊","Gg;":"⋙","Gopf;":"𝔾","GreaterEqual;":"≥","GreaterEqualLess;":"⋛","GreaterFullEqual;":"≧","GreaterGreater;":"⪢","GreaterLess;":"≷","GreaterSlantEqual;":"⩾","GreaterTilde;":"≳","Gscr;":"𝒢","Gt;":"≫","HARDcy;":"Ъ","Hacek;":"ˇ","Hat;":"^","Hcirc;":"Ĥ","Hfr;":"ℌ","HilbertSpace;":"ℋ","Hopf;":"ℍ","HorizontalLine;":"─","Hscr;":"ℋ","Hstrok;":"Ħ","HumpDownHump;":"≎","HumpEqual;":"≏","IEcy;":"Е","IJlig;":"IJ","IOcy;":"Ё","Iacute":"Í","Iacute;":"Í","Icirc":"Î","Icirc;":"Î","Icy;":"И","Idot;":"İ","Ifr;":"ℑ","Igrave":"Ì","Igrave;":"Ì","Im;":"ℑ","Imacr;":"Ī","ImaginaryI;":"ⅈ","Implies;":"⇒","Int;":"∬","Integral;":"∫","Intersection;":"⋂","InvisibleComma;":"⁣","InvisibleTimes;":"⁢","Iogon;":"Į","Iopf;":"𝕀","Iota;":"Ι","Iscr;":"ℐ","Itilde;":"Ĩ","Iukcy;":"І","Iuml":"Ï","Iuml;":"Ï","Jcirc;":"Ĵ","Jcy;":"Й","Jfr;":"𝔍","Jopf;":"𝕁","Jscr;":"𝒥","Jsercy;":"Ј","Jukcy;":"Є","KHcy;":"Х","KJcy;":"Ќ","Kappa;":"Κ","Kcedil;":"Ķ","Kcy;":"К","Kfr;":"𝔎","Kopf;":"𝕂","Kscr;":"𝒦","LJcy;":"Љ","LT":"<","LT;":"<","Lacute;":"Ĺ","Lambda;":"Λ","Lang;":"⟪","Laplacetrf;":"ℒ","Larr;":"↞","Lcaron;":"Ľ","Lcedil;":"Ļ","Lcy;":"Л","LeftAngleBracket;":"⟨","LeftArrow;":"←","LeftArrowBar;":"⇤","LeftArrowRightArrow;":"⇆","LeftCeiling;":"⌈","LeftDoubleBracket;":"⟦","LeftDownTeeVector;":"⥡","LeftDownVector;":"⇃","LeftDownVectorBar;":"⥙","LeftFloor;":"⌊","LeftRightArrow;":"↔","LeftRightVector;":"⥎","LeftTee;":"⊣","LeftTeeArrow;":"↤","LeftTeeVector;":"⥚","LeftTriangle;":"⊲","LeftTriangleBar;":"⧏","LeftTriangleEqual;":"⊴","LeftUpDownVector;":"⥑","LeftUpTeeVector;":"⥠","LeftUpVector;":"↿","LeftUpVectorBar;":"⥘","LeftVector;":"↼","LeftVectorBar;":"⥒","Leftarrow;":"⇐","Leftrightarrow;":"⇔","LessEqualGreater;":"⋚","LessFullEqual;":"≦","LessGreater;":"≶","LessLess;":"⪡","LessSlantEqual;":"⩽","LessTilde;":"≲","Lfr;":"𝔏","Ll;":"⋘","Lleftarrow;":"⇚","Lmidot;":"Ŀ","LongLeftArrow;":"⟵","LongLeftRightArrow;":"⟷","LongRightArrow;":"⟶","Longleftarrow;":"⟸","Longleftrightarrow;":"⟺","Longrightarrow;":"⟹","Lopf;":"𝕃","LowerLeftArrow;":"↙","LowerRightArrow;":"↘","Lscr;":"ℒ","Lsh;":"↰","Lstrok;":"Ł","Lt;":"≪","Map;":"⤅","Mcy;":"М","MediumSpace;":" ","Mellintrf;":"ℳ","Mfr;":"𝔐","MinusPlus;":"∓","Mopf;":"𝕄","Mscr;":"ℳ","Mu;":"Μ","NJcy;":"Њ","Nacute;":"Ń","Ncaron;":"Ň","Ncedil;":"Ņ","Ncy;":"Н","NegativeMediumSpace;":"​","NegativeThickSpace;":"​","NegativeThinSpace;":"​","NegativeVeryThinSpace;":"​","NestedGreaterGreater;":"≫","NestedLessLess;":"≪","NewLine;":"\n","Nfr;":"𝔑","NoBreak;":"⁠","NonBreakingSpace;":" ","Nopf;":"ℕ","Not;":"⫬","NotCongruent;":"≢","NotCupCap;":"≭","NotDoubleVerticalBar;":"∦","NotElement;":"∉","NotEqual;":"≠","NotEqualTilde;":"≂̸","NotExists;":"∄","NotGreater;":"≯","NotGreaterEqual;":"≱","NotGreaterFullEqual;":"≧̸","NotGreaterGreater;":"≫̸","NotGreaterLess;":"≹","NotGreaterSlantEqual;":"⩾̸","NotGreaterTilde;":"≵","NotHumpDownHump;":"≎̸","NotHumpEqual;":"≏̸","NotLeftTriangle;":"⋪","NotLeftTriangleBar;":"⧏̸","NotLeftTriangleEqual;":"⋬","NotLess;":"≮","NotLessEqual;":"≰","NotLessGreater;":"≸","NotLessLess;":"≪̸","NotLessSlantEqual;":"⩽̸","NotLessTilde;":"≴","NotNestedGreaterGreater;":"⪢̸","NotNestedLessLess;":"⪡̸","NotPrecedes;":"⊀","NotPrecedesEqual;":"⪯̸","NotPrecedesSlantEqual;":"⋠","NotReverseElement;":"∌","NotRightTriangle;":"⋫","NotRightTriangleBar;":"⧐̸","NotRightTriangleEqual;":"⋭","NotSquareSubset;":"⊏̸","NotSquareSubsetEqual;":"⋢","NotSquareSuperset;":"⊐̸","NotSquareSupersetEqual;":"⋣","NotSubset;":"⊂⃒","NotSubsetEqual;":"⊈","NotSucceeds;":"⊁","NotSucceedsEqual;":"⪰̸","NotSucceedsSlantEqual;":"⋡","NotSucceedsTilde;":"≿̸","NotSuperset;":"⊃⃒","NotSupersetEqual;":"⊉","NotTilde;":"≁","NotTildeEqual;":"≄","NotTildeFullEqual;":"≇","NotTildeTilde;":"≉","NotVerticalBar;":"∤","Nscr;":"𝒩","Ntilde":"Ñ","Ntilde;":"Ñ","Nu;":"Ν","OElig;":"Œ","Oacute":"Ó","Oacute;":"Ó","Ocirc":"Ô","Ocirc;":"Ô","Ocy;":"О","Odblac;":"Ő","Ofr;":"𝔒","Ograve":"Ò","Ograve;":"Ò","Omacr;":"Ō","Omega;":"Ω","Omicron;":"Ο","Oopf;":"𝕆","OpenCurlyDoubleQuote;":"“","OpenCurlyQuote;":"‘","Or;":"⩔","Oscr;":"𝒪","Oslash":"Ø","Oslash;":"Ø","Otilde":"Õ","Otilde;":"Õ","Otimes;":"⨷","Ouml":"Ö","Ouml;":"Ö","OverBar;":"‾","OverBrace;":"⏞","OverBracket;":"⎴","OverParenthesis;":"⏜","PartialD;":"∂","Pcy;":"П","Pfr;":"𝔓","Phi;":"Φ","Pi;":"Π","PlusMinus;":"±","Poincareplane;":"ℌ","Popf;":"ℙ","Pr;":"⪻","Precedes;":"≺","PrecedesEqual;":"⪯","PrecedesSlantEqual;":"≼","PrecedesTilde;":"≾","Prime;":"″","Product;":"∏","Proportion;":"∷","Proportional;":"∝","Pscr;":"𝒫","Psi;":"Ψ","QUOT":"\"","QUOT;":"\"","Qfr;":"𝔔","Qopf;":"ℚ","Qscr;":"𝒬","RBarr;":"⤐","REG":"®","REG;":"®","Racute;":"Ŕ","Rang;":"⟫","Rarr;":"↠","Rarrtl;":"⤖","Rcaron;":"Ř","Rcedil;":"Ŗ","Rcy;":"Р","Re;":"ℜ","ReverseElement;":"∋","ReverseEquilibrium;":"⇋","ReverseUpEquilibrium;":"⥯","Rfr;":"ℜ","Rho;":"Ρ","RightAngleBracket;":"⟩","RightArrow;":"→","RightArrowBar;":"⇥","RightArrowLeftArrow;":"⇄","RightCeiling;":"⌉","RightDoubleBracket;":"⟧","RightDownTeeVector;":"⥝","RightDownVector;":"⇂","RightDownVectorBar;":"⥕","RightFloor;":"⌋","RightTee;":"⊢","RightTeeArrow;":"↦","RightTeeVector;":"⥛","RightTriangle;":"⊳","RightTriangleBar;":"⧐","RightTriangleEqual;":"⊵","RightUpDownVector;":"⥏","RightUpTeeVector;":"⥜","RightUpVector;":"↾","RightUpVectorBar;":"⥔","RightVector;":"⇀","RightVectorBar;":"⥓","Rightarrow;":"⇒","Ropf;":"ℝ","RoundImplies;":"⥰","Rrightarrow;":"⇛","Rscr;":"ℛ","Rsh;":"↱","RuleDelayed;":"⧴","SHCHcy;":"Щ","SHcy;":"Ш","SOFTcy;":"Ь","Sacute;":"Ś","Sc;":"⪼","Scaron;":"Š","Scedil;":"Ş","Scirc;":"Ŝ","Scy;":"С","Sfr;":"𝔖","ShortDownArrow;":"↓","ShortLeftArrow;":"←","ShortRightArrow;":"→","ShortUpArrow;":"↑","Sigma;":"Σ","SmallCircle;":"∘","Sopf;":"𝕊","Sqrt;":"√","Square;":"□","SquareIntersection;":"⊓","SquareSubset;":"⊏","SquareSubsetEqual;":"⊑","SquareSuperset;":"⊐","SquareSupersetEqual;":"⊒","SquareUnion;":"⊔","Sscr;":"𝒮","Star;":"⋆","Sub;":"⋐","Subset;":"⋐","SubsetEqual;":"⊆","Succeeds;":"≻","SucceedsEqual;":"⪰","SucceedsSlantEqual;":"≽","SucceedsTilde;":"≿","SuchThat;":"∋","Sum;":"∑","Sup;":"⋑","Superset;":"⊃","SupersetEqual;":"⊇","Supset;":"⋑","THORN":"Þ","THORN;":"Þ","TRADE;":"™","TSHcy;":"Ћ","TScy;":"Ц","Tab;":"\t","Tau;":"Τ","Tcaron;":"Ť","Tcedil;":"Ţ","Tcy;":"Т","Tfr;":"𝔗","Therefore;":"∴","Theta;":"Θ","ThickSpace;":"  ","ThinSpace;":" ","Tilde;":"∼","TildeEqual;":"≃","TildeFullEqual;":"≅","TildeTilde;":"≈","Topf;":"𝕋","TripleDot;":"⃛","Tscr;":"𝒯","Tstrok;":"Ŧ","Uacute":"Ú","Uacute;":"Ú","Uarr;":"↟","Uarrocir;":"⥉","Ubrcy;":"Ў","Ubreve;":"Ŭ","Ucirc":"Û","Ucirc;":"Û","Ucy;":"У","Udblac;":"Ű","Ufr;":"𝔘","Ugrave":"Ù","Ugrave;":"Ù","Umacr;":"Ū","UnderBar;":"_","UnderBrace;":"⏟","UnderBracket;":"⎵","UnderParenthesis;":"⏝","Union;":"⋃","UnionPlus;":"⊎","Uogon;":"Ų","Uopf;":"𝕌","UpArrow;":"↑","UpArrowBar;":"⤒","UpArrowDownArrow;":"⇅","UpDownArrow;":"↕","UpEquilibrium;":"⥮","UpTee;":"⊥","UpTeeArrow;":"↥","Uparrow;":"⇑","Updownarrow;":"⇕","UpperLeftArrow;":"↖","UpperRightArrow;":"↗","Upsi;":"ϒ","Upsilon;":"Υ","Uring;":"Ů","Uscr;":"𝒰","Utilde;":"Ũ","Uuml":"Ü","Uuml;":"Ü","VDash;":"⊫","Vbar;":"⫫","Vcy;":"В","Vdash;":"⊩","Vdashl;":"⫦","Vee;":"⋁","Verbar;":"‖","Vert;":"‖","VerticalBar;":"∣","VerticalLine;":"|","VerticalSeparator;":"❘","VerticalTilde;":"≀","VeryThinSpace;":" ","Vfr;":"𝔙","Vopf;":"𝕍","Vscr;":"𝒱","Vvdash;":"⊪","Wcirc;":"Ŵ","Wedge;":"⋀","Wfr;":"𝔚","Wopf;":"𝕎","Wscr;":"𝒲","Xfr;":"𝔛","Xi;":"Ξ","Xopf;":"𝕏","Xscr;":"𝒳","YAcy;":"Я","YIcy;":"Ї","YUcy;":"Ю","Yacute":"Ý","Yacute;":"Ý","Ycirc;":"Ŷ","Ycy;":"Ы","Yfr;":"𝔜","Yopf;":"𝕐","Yscr;":"𝒴","Yuml;":"Ÿ","ZHcy;":"Ж","Zacute;":"Ź","Zcaron;":"Ž","Zcy;":"З","Zdot;":"Ż","ZeroWidthSpace;":"​","Zeta;":"Ζ","Zfr;":"ℨ","Zopf;":"ℤ","Zscr;":"𝒵","aacute":"á","aacute;":"á","abreve;":"ă","ac;":"∾","acE;":"∾̳","acd;":"∿","acirc":"â","acirc;":"â","acute":"´","acute;":"´","acy;":"а","aelig":"æ","aelig;":"æ","af;":"⁡","afr;":"𝔞","agrave":"à","agrave;":"à","alefsym;":"ℵ","aleph;":"ℵ","alpha;":"α","amacr;":"ā","amalg;":"⨿","amp":"&","amp;":"&","and;":"∧","andand;":"⩕","andd;":"⩜","andslope;":"⩘","andv;":"⩚","ang;":"∠","ange;":"⦤","angle;":"∠","angmsd;":"∡","angmsdaa;":"⦨","angmsdab;":"⦩","angmsdac;":"⦪","angmsdad;":"⦫","angmsdae;":"⦬","angmsdaf;":"⦭","angmsdag;":"⦮","angmsdah;":"⦯","angrt;":"∟","angrtvb;":"⊾","angrtvbd;":"⦝","angsph;":"∢","angst;":"Å","angzarr;":"⍼","aogon;":"ą","aopf;":"𝕒","ap;":"≈","apE;":"⩰","apacir;":"⩯","ape;":"≊","apid;":"≋","apos;":"'","approx;":"≈","approxeq;":"≊","aring":"å","aring;":"å","ascr;":"𝒶","ast;":"*","asymp;":"≈","asympeq;":"≍","atilde":"ã","atilde;":"ã","auml":"ä","auml;":"ä","awconint;":"∳","awint;":"⨑","bNot;":"⫭","backcong;":"≌","backepsilon;":"϶","backprime;":"‵","backsim;":"∽","backsimeq;":"⋍","barvee;":"⊽","barwed;":"⌅","barwedge;":"⌅","bbrk;":"⎵","bbrktbrk;":"⎶","bcong;":"≌","bcy;":"б","bdquo;":"„","becaus;":"∵","because;":"∵","bemptyv;":"⦰","bepsi;":"϶","bernou;":"ℬ","beta;":"β","beth;":"ℶ","between;":"≬","bfr;":"𝔟","bigcap;":"⋂","bigcirc;":"◯","bigcup;":"⋃","bigodot;":"⨀","bigoplus;":"⨁","bigotimes;":"⨂","bigsqcup;":"⨆","bigstar;":"★","bigtriangledown;":"▽","bigtriangleup;":"△","biguplus;":"⨄","bigvee;":"⋁","bigwedge;":"⋀","bkarow;":"⤍","blacklozenge;":"⧫","blacksquare;":"▪","blacktriangle;":"▴","blacktriangledown;":"▾","blacktriangleleft;":"◂","blacktriangleright;":"▸","blank;":"␣","blk12;":"▒","blk14;":"░","blk34;":"▓","block;":"█","bne;":"=⃥","bnequiv;":"≡⃥","bnot;":"⌐","bopf;":"𝕓","bot;":"⊥","bottom;":"⊥","bowtie;":"⋈","boxDL;":"╗","boxDR;":"╔","boxDl;":"╖","boxDr;":"╓","boxH;":"═","boxHD;":"╦","boxHU;":"╩","boxHd;":"╤","boxHu;":"╧","boxUL;":"╝","boxUR;":"╚","boxUl;":"╜","boxUr;":"╙","boxV;":"║","boxVH;":"╬","boxVL;":"╣","boxVR;":"╠","boxVh;":"╫","boxVl;":"╢","boxVr;":"╟","boxbox;":"⧉","boxdL;":"╕","boxdR;":"╒","boxdl;":"┐","boxdr;":"┌","boxh;":"─","boxhD;":"╥","boxhU;":"╨","boxhd;":"┬","boxhu;":"┴","boxminus;":"⊟","boxplus;":"⊞","boxtimes;":"⊠","boxuL;":"╛","boxuR;":"╘","boxul;":"┘","boxur;":"└","boxv;":"│","boxvH;":"╪","boxvL;":"╡","boxvR;":"╞","boxvh;":"┼","boxvl;":"┤","boxvr;":"├","bprime;":"‵","breve;":"˘","brvbar":"¦","brvbar;":"¦","bscr;":"𝒷","bsemi;":"⁏","bsim;":"∽","bsime;":"⋍","bsol;":"\\","bsolb;":"⧅","bsolhsub;":"⟈","bull;":"•","bullet;":"•","bump;":"≎","bumpE;":"⪮","bumpe;":"≏","bumpeq;":"≏","cacute;":"ć","cap;":"∩","capand;":"⩄","capbrcup;":"⩉","capcap;":"⩋","capcup;":"⩇","capdot;":"⩀","caps;":"∩︀","caret;":"⁁","caron;":"ˇ","ccaps;":"⩍","ccaron;":"č","ccedil":"ç","ccedil;":"ç","ccirc;":"ĉ","ccups;":"⩌","ccupssm;":"⩐","cdot;":"ċ","cedil":"¸","cedil;":"¸","cemptyv;":"⦲","cent":"¢","cent;":"¢","centerdot;":"·","cfr;":"𝔠","chcy;":"ч","check;":"✓","checkmark;":"✓","chi;":"χ","cir;":"○","cirE;":"⧃","circ;":"ˆ","circeq;":"≗","circlearrowleft;":"↺","circlearrowright;":"↻","circledR;":"®","circledS;":"Ⓢ","circledast;":"⊛","circledcirc;":"⊚","circleddash;":"⊝","cire;":"≗","cirfnint;":"⨐","cirmid;":"⫯","cirscir;":"⧂","clubs;":"♣","clubsuit;":"♣","colon;":":","colone;":"≔","coloneq;":"≔","comma;":",","commat;":"@","comp;":"∁","compfn;":"∘","complement;":"∁","complexes;":"ℂ","cong;":"≅","congdot;":"⩭","conint;":"∮","copf;":"𝕔","coprod;":"∐","copy":"©","copy;":"©","copysr;":"℗","crarr;":"↵","cross;":"✗","cscr;":"𝒸","csub;":"⫏","csube;":"⫑","csup;":"⫐","csupe;":"⫒","ctdot;":"⋯","cudarrl;":"⤸","cudarrr;":"⤵","cuepr;":"⋞","cuesc;":"⋟","cularr;":"↶","cularrp;":"⤽","cup;":"∪","cupbrcap;":"⩈","cupcap;":"⩆","cupcup;":"⩊","cupdot;":"⊍","cupor;":"⩅","cups;":"∪︀","curarr;":"↷","curarrm;":"⤼","curlyeqprec;":"⋞","curlyeqsucc;":"⋟","curlyvee;":"⋎","curlywedge;":"⋏","curren":"¤","curren;":"¤","curvearrowleft;":"↶","curvearrowright;":"↷","cuvee;":"⋎","cuwed;":"⋏","cwconint;":"∲","cwint;":"∱","cylcty;":"⌭","dArr;":"⇓","dHar;":"⥥","dagger;":"†","daleth;":"ℸ","darr;":"↓","dash;":"‐","dashv;":"⊣","dbkarow;":"⤏","dblac;":"˝","dcaron;":"ď","dcy;":"д","dd;":"ⅆ","ddagger;":"‡","ddarr;":"⇊","ddotseq;":"⩷","deg":"°","deg;":"°","delta;":"δ","demptyv;":"⦱","dfisht;":"⥿","dfr;":"𝔡","dharl;":"⇃","dharr;":"⇂","diam;":"⋄","diamond;":"⋄","diamondsuit;":"♦","diams;":"♦","die;":"¨","digamma;":"ϝ","disin;":"⋲","div;":"÷","divide":"÷","divide;":"÷","divideontimes;":"⋇","divonx;":"⋇","djcy;":"ђ","dlcorn;":"⌞","dlcrop;":"⌍","dollar;":"$","dopf;":"𝕕","dot;":"˙","doteq;":"≐","doteqdot;":"≑","dotminus;":"∸","dotplus;":"∔","dotsquare;":"⊡","doublebarwedge;":"⌆","downarrow;":"↓","downdownarrows;":"⇊","downharpoonleft;":"⇃","downharpoonright;":"⇂","drbkarow;":"⤐","drcorn;":"⌟","drcrop;":"⌌","dscr;":"𝒹","dscy;":"ѕ","dsol;":"⧶","dstrok;":"đ","dtdot;":"⋱","dtri;":"▿","dtrif;":"▾","duarr;":"⇵","duhar;":"⥯","dwangle;":"⦦","dzcy;":"џ","dzigrarr;":"⟿","eDDot;":"⩷","eDot;":"≑","eacute":"é","eacute;":"é","easter;":"⩮","ecaron;":"ě","ecir;":"≖","ecirc":"ê","ecirc;":"ê","ecolon;":"≕","ecy;":"э","edot;":"ė","ee;":"ⅇ","efDot;":"≒","efr;":"𝔢","eg;":"⪚","egrave":"è","egrave;":"è","egs;":"⪖","egsdot;":"⪘","el;":"⪙","elinters;":"⏧","ell;":"ℓ","els;":"⪕","elsdot;":"⪗","emacr;":"ē","empty;":"∅","emptyset;":"∅","emptyv;":"∅","emsp13;":" ","emsp14;":" ","emsp;":" ","eng;":"ŋ","ensp;":" ","eogon;":"ę","eopf;":"𝕖","epar;":"⋕","eparsl;":"⧣","eplus;":"⩱","epsi;":"ε","epsilon;":"ε","epsiv;":"ϵ","eqcirc;":"≖","eqcolon;":"≕","eqsim;":"≂","eqslantgtr;":"⪖","eqslantless;":"⪕","equals;":"=","equest;":"≟","equiv;":"≡","equivDD;":"⩸","eqvparsl;":"⧥","erDot;":"≓","erarr;":"⥱","escr;":"ℯ","esdot;":"≐","esim;":"≂","eta;":"η","eth":"ð","eth;":"ð","euml":"ë","euml;":"ë","euro;":"€","excl;":"!","exist;":"∃","expectation;":"ℰ","exponentiale;":"ⅇ","fallingdotseq;":"≒","fcy;":"ф","female;":"♀","ffilig;":"ffi","fflig;":"ff","ffllig;":"ffl","ffr;":"𝔣","filig;":"fi","fjlig;":"fj","flat;":"♭","fllig;":"fl","fltns;":"▱","fnof;":"ƒ","fopf;":"𝕗","forall;":"∀","fork;":"⋔","forkv;":"⫙","fpartint;":"⨍","frac12":"½","frac12;":"½","frac13;":"⅓","frac14":"¼","frac14;":"¼","frac15;":"⅕","frac16;":"⅙","frac18;":"⅛","frac23;":"⅔","frac25;":"⅖","frac34":"¾","frac34;":"¾","frac35;":"⅗","frac38;":"⅜","frac45;":"⅘","frac56;":"⅚","frac58;":"⅝","frac78;":"⅞","frasl;":"⁄","frown;":"⌢","fscr;":"𝒻","gE;":"≧","gEl;":"⪌","gacute;":"ǵ","gamma;":"γ","gammad;":"ϝ","gap;":"⪆","gbreve;":"ğ","gcirc;":"ĝ","gcy;":"г","gdot;":"ġ","ge;":"≥","gel;":"⋛","geq;":"≥","geqq;":"≧","geqslant;":"⩾","ges;":"⩾","gescc;":"⪩","gesdot;":"⪀","gesdoto;":"⪂","gesdotol;":"⪄","gesl;":"⋛︀","gesles;":"⪔","gfr;":"𝔤","gg;":"≫","ggg;":"⋙","gimel;":"ℷ","gjcy;":"ѓ","gl;":"≷","glE;":"⪒","gla;":"⪥","glj;":"⪤","gnE;":"≩","gnap;":"⪊","gnapprox;":"⪊","gne;":"⪈","gneq;":"⪈","gneqq;":"≩","gnsim;":"⋧","gopf;":"𝕘","grave;":"`","gscr;":"ℊ","gsim;":"≳","gsime;":"⪎","gsiml;":"⪐","gt":">","gt;":">","gtcc;":"⪧","gtcir;":"⩺","gtdot;":"⋗","gtlPar;":"⦕","gtquest;":"⩼","gtrapprox;":"⪆","gtrarr;":"⥸","gtrdot;":"⋗","gtreqless;":"⋛","gtreqqless;":"⪌","gtrless;":"≷","gtrsim;":"≳","gvertneqq;":"≩︀","gvnE;":"≩︀","hArr;":"⇔","hairsp;":" ","half;":"½","hamilt;":"ℋ","hardcy;":"ъ","harr;":"↔","harrcir;":"⥈","harrw;":"↭","hbar;":"ℏ","hcirc;":"ĥ","hearts;":"♥","heartsuit;":"♥","hellip;":"…","hercon;":"⊹","hfr;":"𝔥","hksearow;":"⤥","hkswarow;":"⤦","hoarr;":"⇿","homtht;":"∻","hookleftarrow;":"↩","hookrightarrow;":"↪","hopf;":"𝕙","horbar;":"―","hscr;":"𝒽","hslash;":"ℏ","hstrok;":"ħ","hybull;":"⁃","hyphen;":"‐","iacute":"í","iacute;":"í","ic;":"⁣","icirc":"î","icirc;":"î","icy;":"и","iecy;":"е","iexcl":"¡","iexcl;":"¡","iff;":"⇔","ifr;":"𝔦","igrave":"ì","igrave;":"ì","ii;":"ⅈ","iiiint;":"⨌","iiint;":"∭","iinfin;":"⧜","iiota;":"℩","ijlig;":"ij","imacr;":"ī","image;":"ℑ","imagline;":"ℐ","imagpart;":"ℑ","imath;":"ı","imof;":"⊷","imped;":"Ƶ","in;":"∈","incare;":"℅","infin;":"∞","infintie;":"⧝","inodot;":"ı","int;":"∫","intcal;":"⊺","integers;":"ℤ","intercal;":"⊺","intlarhk;":"⨗","intprod;":"⨼","iocy;":"ё","iogon;":"į","iopf;":"𝕚","iota;":"ι","iprod;":"⨼","iquest":"¿","iquest;":"¿","iscr;":"𝒾","isin;":"∈","isinE;":"⋹","isindot;":"⋵","isins;":"⋴","isinsv;":"⋳","isinv;":"∈","it;":"⁢","itilde;":"ĩ","iukcy;":"і","iuml":"ï","iuml;":"ï","jcirc;":"ĵ","jcy;":"й","jfr;":"𝔧","jmath;":"ȷ","jopf;":"𝕛","jscr;":"𝒿","jsercy;":"ј","jukcy;":"є","kappa;":"κ","kappav;":"ϰ","kcedil;":"ķ","kcy;":"к","kfr;":"𝔨","kgreen;":"ĸ","khcy;":"х","kjcy;":"ќ","kopf;":"𝕜","kscr;":"𝓀","lAarr;":"⇚","lArr;":"⇐","lAtail;":"⤛","lBarr;":"⤎","lE;":"≦","lEg;":"⪋","lHar;":"⥢","lacute;":"ĺ","laemptyv;":"⦴","lagran;":"ℒ","lambda;":"λ","lang;":"⟨","langd;":"⦑","langle;":"⟨","lap;":"⪅","laquo":"«","laquo;":"«","larr;":"←","larrb;":"⇤","larrbfs;":"⤟","larrfs;":"⤝","larrhk;":"↩","larrlp;":"↫","larrpl;":"⤹","larrsim;":"⥳","larrtl;":"↢","lat;":"⪫","latail;":"⤙","late;":"⪭","lates;":"⪭︀","lbarr;":"⤌","lbbrk;":"❲","lbrace;":"{","lbrack;":"[","lbrke;":"⦋","lbrksld;":"⦏","lbrkslu;":"⦍","lcaron;":"ľ","lcedil;":"ļ","lceil;":"⌈","lcub;":"{","lcy;":"л","ldca;":"⤶","ldquo;":"“","ldquor;":"„","ldrdhar;":"⥧","ldrushar;":"⥋","ldsh;":"↲","le;":"≤","leftarrow;":"←","leftarrowtail;":"↢","leftharpoondown;":"↽","leftharpoonup;":"↼","leftleftarrows;":"⇇","leftrightarrow;":"↔","leftrightarrows;":"⇆","leftrightharpoons;":"⇋","leftrightsquigarrow;":"↭","leftthreetimes;":"⋋","leg;":"⋚","leq;":"≤","leqq;":"≦","leqslant;":"⩽","les;":"⩽","lescc;":"⪨","lesdot;":"⩿","lesdoto;":"⪁","lesdotor;":"⪃","lesg;":"⋚︀","lesges;":"⪓","lessapprox;":"⪅","lessdot;":"⋖","lesseqgtr;":"⋚","lesseqqgtr;":"⪋","lessgtr;":"≶","lesssim;":"≲","lfisht;":"⥼","lfloor;":"⌊","lfr;":"𝔩","lg;":"≶","lgE;":"⪑","lhard;":"↽","lharu;":"↼","lharul;":"⥪","lhblk;":"▄","ljcy;":"љ","ll;":"≪","llarr;":"⇇","llcorner;":"⌞","llhard;":"⥫","lltri;":"◺","lmidot;":"ŀ","lmoust;":"⎰","lmoustache;":"⎰","lnE;":"≨","lnap;":"⪉","lnapprox;":"⪉","lne;":"⪇","lneq;":"⪇","lneqq;":"≨","lnsim;":"⋦","loang;":"⟬","loarr;":"⇽","lobrk;":"⟦","longleftarrow;":"⟵","longleftrightarrow;":"⟷","longmapsto;":"⟼","longrightarrow;":"⟶","looparrowleft;":"↫","looparrowright;":"↬","lopar;":"⦅","lopf;":"𝕝","loplus;":"⨭","lotimes;":"⨴","lowast;":"∗","lowbar;":"_","loz;":"◊","lozenge;":"◊","lozf;":"⧫","lpar;":"(","lparlt;":"⦓","lrarr;":"⇆","lrcorner;":"⌟","lrhar;":"⇋","lrhard;":"⥭","lrm;":"‎","lrtri;":"⊿","lsaquo;":"‹","lscr;":"𝓁","lsh;":"↰","lsim;":"≲","lsime;":"⪍","lsimg;":"⪏","lsqb;":"[","lsquo;":"‘","lsquor;":"‚","lstrok;":"ł","lt":"<","lt;":"<","ltcc;":"⪦","ltcir;":"⩹","ltdot;":"⋖","lthree;":"⋋","ltimes;":"⋉","ltlarr;":"⥶","ltquest;":"⩻","ltrPar;":"⦖","ltri;":"◃","ltrie;":"⊴","ltrif;":"◂","lurdshar;":"⥊","luruhar;":"⥦","lvertneqq;":"≨︀","lvnE;":"≨︀","mDDot;":"∺","macr":"¯","macr;":"¯","male;":"♂","malt;":"✠","maltese;":"✠","map;":"↦","mapsto;":"↦","mapstodown;":"↧","mapstoleft;":"↤","mapstoup;":"↥","marker;":"▮","mcomma;":"⨩","mcy;":"м","mdash;":"—","measuredangle;":"∡","mfr;":"𝔪","mho;":"℧","micro":"µ","micro;":"µ","mid;":"∣","midast;":"*","midcir;":"⫰","middot":"·","middot;":"·","minus;":"−","minusb;":"⊟","minusd;":"∸","minusdu;":"⨪","mlcp;":"⫛","mldr;":"…","mnplus;":"∓","models;":"⊧","mopf;":"𝕞","mp;":"∓","mscr;":"𝓂","mstpos;":"∾","mu;":"μ","multimap;":"⊸","mumap;":"⊸","nGg;":"⋙̸","nGt;":"≫⃒","nGtv;":"≫̸","nLeftarrow;":"⇍","nLeftrightarrow;":"⇎","nLl;":"⋘̸","nLt;":"≪⃒","nLtv;":"≪̸","nRightarrow;":"⇏","nVDash;":"⊯","nVdash;":"⊮","nabla;":"∇","nacute;":"ń","nang;":"∠⃒","nap;":"≉","napE;":"⩰̸","napid;":"≋̸","napos;":"ʼn","napprox;":"≉","natur;":"♮","natural;":"♮","naturals;":"ℕ","nbsp":" ","nbsp;":" ","nbump;":"≎̸","nbumpe;":"≏̸","ncap;":"⩃","ncaron;":"ň","ncedil;":"ņ","ncong;":"≇","ncongdot;":"⩭̸","ncup;":"⩂","ncy;":"н","ndash;":"–","ne;":"≠","neArr;":"⇗","nearhk;":"⤤","nearr;":"↗","nearrow;":"↗","nedot;":"≐̸","nequiv;":"≢","nesear;":"⤨","nesim;":"≂̸","nexist;":"∄","nexists;":"∄","nfr;":"𝔫","ngE;":"≧̸","nge;":"≱","ngeq;":"≱","ngeqq;":"≧̸","ngeqslant;":"⩾̸","nges;":"⩾̸","ngsim;":"≵","ngt;":"≯","ngtr;":"≯","nhArr;":"⇎","nharr;":"↮","nhpar;":"⫲","ni;":"∋","nis;":"⋼","nisd;":"⋺","niv;":"∋","njcy;":"њ","nlArr;":"⇍","nlE;":"≦̸","nlarr;":"↚","nldr;":"‥","nle;":"≰","nleftarrow;":"↚","nleftrightarrow;":"↮","nleq;":"≰","nleqq;":"≦̸","nleqslant;":"⩽̸","nles;":"⩽̸","nless;":"≮","nlsim;":"≴","nlt;":"≮","nltri;":"⋪","nltrie;":"⋬","nmid;":"∤","nopf;":"𝕟","not":"¬","not;":"¬","notin;":"∉","notinE;":"⋹̸","notindot;":"⋵̸","notinva;":"∉","notinvb;":"⋷","notinvc;":"⋶","notni;":"∌","notniva;":"∌","notnivb;":"⋾","notnivc;":"⋽","npar;":"∦","nparallel;":"∦","nparsl;":"⫽⃥","npart;":"∂̸","npolint;":"⨔","npr;":"⊀","nprcue;":"⋠","npre;":"⪯̸","nprec;":"⊀","npreceq;":"⪯̸","nrArr;":"⇏","nrarr;":"↛","nrarrc;":"⤳̸","nrarrw;":"↝̸","nrightarrow;":"↛","nrtri;":"⋫","nrtrie;":"⋭","nsc;":"⊁","nsccue;":"⋡","nsce;":"⪰̸","nscr;":"𝓃","nshortmid;":"∤","nshortparallel;":"∦","nsim;":"≁","nsime;":"≄","nsimeq;":"≄","nsmid;":"∤","nspar;":"∦","nsqsube;":"⋢","nsqsupe;":"⋣","nsub;":"⊄","nsubE;":"⫅̸","nsube;":"⊈","nsubset;":"⊂⃒","nsubseteq;":"⊈","nsubseteqq;":"⫅̸","nsucc;":"⊁","nsucceq;":"⪰̸","nsup;":"⊅","nsupE;":"⫆̸","nsupe;":"⊉","nsupset;":"⊃⃒","nsupseteq;":"⊉","nsupseteqq;":"⫆̸","ntgl;":"≹","ntilde":"ñ","ntilde;":"ñ","ntlg;":"≸","ntriangleleft;":"⋪","ntrianglelefteq;":"⋬","ntriangleright;":"⋫","ntrianglerighteq;":"⋭","nu;":"ν","num;":"#","numero;":"№","numsp;":" ","nvDash;":"⊭","nvHarr;":"⤄","nvap;":"≍⃒","nvdash;":"⊬","nvge;":"≥⃒","nvgt;":">⃒","nvinfin;":"⧞","nvlArr;":"⤂","nvle;":"≤⃒","nvlt;":"<⃒","nvltrie;":"⊴⃒","nvrArr;":"⤃","nvrtrie;":"⊵⃒","nvsim;":"∼⃒","nwArr;":"⇖","nwarhk;":"⤣","nwarr;":"↖","nwarrow;":"↖","nwnear;":"⤧","oS;":"Ⓢ","oacute":"ó","oacute;":"ó","oast;":"⊛","ocir;":"⊚","ocirc":"ô","ocirc;":"ô","ocy;":"о","odash;":"⊝","odblac;":"ő","odiv;":"⨸","odot;":"⊙","odsold;":"⦼","oelig;":"œ","ofcir;":"⦿","ofr;":"𝔬","ogon;":"˛","ograve":"ò","ograve;":"ò","ogt;":"⧁","ohbar;":"⦵","ohm;":"Ω","oint;":"∮","olarr;":"↺","olcir;":"⦾","olcross;":"⦻","oline;":"‾","olt;":"⧀","omacr;":"ō","omega;":"ω","omicron;":"ο","omid;":"⦶","ominus;":"⊖","oopf;":"𝕠","opar;":"⦷","operp;":"⦹","oplus;":"⊕","or;":"∨","orarr;":"↻","ord;":"⩝","order;":"ℴ","orderof;":"ℴ","ordf":"ª","ordf;":"ª","ordm":"º","ordm;":"º","origof;":"⊶","oror;":"⩖","orslope;":"⩗","orv;":"⩛","oscr;":"ℴ","oslash":"ø","oslash;":"ø","osol;":"⊘","otilde":"õ","otilde;":"õ","otimes;":"⊗","otimesas;":"⨶","ouml":"ö","ouml;":"ö","ovbar;":"⌽","par;":"∥","para":"¶","para;":"¶","parallel;":"∥","parsim;":"⫳","parsl;":"⫽","part;":"∂","pcy;":"п","percnt;":"%","period;":".","permil;":"‰","perp;":"⊥","pertenk;":"‱","pfr;":"𝔭","phi;":"φ","phiv;":"ϕ","phmmat;":"ℳ","phone;":"☎","pi;":"π","pitchfork;":"⋔","piv;":"ϖ","planck;":"ℏ","planckh;":"ℎ","plankv;":"ℏ","plus;":"+","plusacir;":"⨣","plusb;":"⊞","pluscir;":"⨢","plusdo;":"∔","plusdu;":"⨥","pluse;":"⩲","plusmn":"±","plusmn;":"±","plussim;":"⨦","plustwo;":"⨧","pm;":"±","pointint;":"⨕","popf;":"𝕡","pound":"£","pound;":"£","pr;":"≺","prE;":"⪳","prap;":"⪷","prcue;":"≼","pre;":"⪯","prec;":"≺","precapprox;":"⪷","preccurlyeq;":"≼","preceq;":"⪯","precnapprox;":"⪹","precneqq;":"⪵","precnsim;":"⋨","precsim;":"≾","prime;":"′","primes;":"ℙ","prnE;":"⪵","prnap;":"⪹","prnsim;":"⋨","prod;":"∏","profalar;":"⌮","profline;":"⌒","profsurf;":"⌓","prop;":"∝","propto;":"∝","prsim;":"≾","prurel;":"⊰","pscr;":"𝓅","psi;":"ψ","puncsp;":" ","qfr;":"𝔮","qint;":"⨌","qopf;":"𝕢","qprime;":"⁗","qscr;":"𝓆","quaternions;":"ℍ","quatint;":"⨖","quest;":"?","questeq;":"≟","quot":"\"","quot;":"\"","rAarr;":"⇛","rArr;":"⇒","rAtail;":"⤜","rBarr;":"⤏","rHar;":"⥤","race;":"∽̱","racute;":"ŕ","radic;":"√","raemptyv;":"⦳","rang;":"⟩","rangd;":"⦒","range;":"⦥","rangle;":"⟩","raquo":"»","raquo;":"»","rarr;":"→","rarrap;":"⥵","rarrb;":"⇥","rarrbfs;":"⤠","rarrc;":"⤳","rarrfs;":"⤞","rarrhk;":"↪","rarrlp;":"↬","rarrpl;":"⥅","rarrsim;":"⥴","rarrtl;":"↣","rarrw;":"↝","ratail;":"⤚","ratio;":"∶","rationals;":"ℚ","rbarr;":"⤍","rbbrk;":"❳","rbrace;":"}","rbrack;":"]","rbrke;":"⦌","rbrksld;":"⦎","rbrkslu;":"⦐","rcaron;":"ř","rcedil;":"ŗ","rceil;":"⌉","rcub;":"}","rcy;":"р","rdca;":"⤷","rdldhar;":"⥩","rdquo;":"”","rdquor;":"”","rdsh;":"↳","real;":"ℜ","realine;":"ℛ","realpart;":"ℜ","reals;":"ℝ","rect;":"▭","reg":"®","reg;":"®","rfisht;":"⥽","rfloor;":"⌋","rfr;":"𝔯","rhard;":"⇁","rharu;":"⇀","rharul;":"⥬","rho;":"ρ","rhov;":"ϱ","rightarrow;":"→","rightarrowtail;":"↣","rightharpoondown;":"⇁","rightharpoonup;":"⇀","rightleftarrows;":"⇄","rightleftharpoons;":"⇌","rightrightarrows;":"⇉","rightsquigarrow;":"↝","rightthreetimes;":"⋌","ring;":"˚","risingdotseq;":"≓","rlarr;":"⇄","rlhar;":"⇌","rlm;":"‏","rmoust;":"⎱","rmoustache;":"⎱","rnmid;":"⫮","roang;":"⟭","roarr;":"⇾","robrk;":"⟧","ropar;":"⦆","ropf;":"𝕣","roplus;":"⨮","rotimes;":"⨵","rpar;":")","rpargt;":"⦔","rppolint;":"⨒","rrarr;":"⇉","rsaquo;":"›","rscr;":"𝓇","rsh;":"↱","rsqb;":"]","rsquo;":"’","rsquor;":"’","rthree;":"⋌","rtimes;":"⋊","rtri;":"▹","rtrie;":"⊵","rtrif;":"▸","rtriltri;":"⧎","ruluhar;":"⥨","rx;":"℞","sacute;":"ś","sbquo;":"‚","sc;":"≻","scE;":"⪴","scap;":"⪸","scaron;":"š","sccue;":"≽","sce;":"⪰","scedil;":"ş","scirc;":"ŝ","scnE;":"⪶","scnap;":"⪺","scnsim;":"⋩","scpolint;":"⨓","scsim;":"≿","scy;":"с","sdot;":"⋅","sdotb;":"⊡","sdote;":"⩦","seArr;":"⇘","searhk;":"⤥","searr;":"↘","searrow;":"↘","sect":"§","sect;":"§","semi;":";","seswar;":"⤩","setminus;":"∖","setmn;":"∖","sext;":"✶","sfr;":"𝔰","sfrown;":"⌢","sharp;":"♯","shchcy;":"щ","shcy;":"ш","shortmid;":"∣","shortparallel;":"∥","shy":"­","shy;":"­","sigma;":"σ","sigmaf;":"ς","sigmav;":"ς","sim;":"∼","simdot;":"⩪","sime;":"≃","simeq;":"≃","simg;":"⪞","simgE;":"⪠","siml;":"⪝","simlE;":"⪟","simne;":"≆","simplus;":"⨤","simrarr;":"⥲","slarr;":"←","smallsetminus;":"∖","smashp;":"⨳","smeparsl;":"⧤","smid;":"∣","smile;":"⌣","smt;":"⪪","smte;":"⪬","smtes;":"⪬︀","softcy;":"ь","sol;":"/","solb;":"⧄","solbar;":"⌿","sopf;":"𝕤","spades;":"♠","spadesuit;":"♠","spar;":"∥","sqcap;":"⊓","sqcaps;":"⊓︀","sqcup;":"⊔","sqcups;":"⊔︀","sqsub;":"⊏","sqsube;":"⊑","sqsubset;":"⊏","sqsubseteq;":"⊑","sqsup;":"⊐","sqsupe;":"⊒","sqsupset;":"⊐","sqsupseteq;":"⊒","squ;":"□","square;":"□","squarf;":"▪","squf;":"▪","srarr;":"→","sscr;":"𝓈","ssetmn;":"∖","ssmile;":"⌣","sstarf;":"⋆","star;":"☆","starf;":"★","straightepsilon;":"ϵ","straightphi;":"ϕ","strns;":"¯","sub;":"⊂","subE;":"⫅","subdot;":"⪽","sube;":"⊆","subedot;":"⫃","submult;":"⫁","subnE;":"⫋","subne;":"⊊","subplus;":"⪿","subrarr;":"⥹","subset;":"⊂","subseteq;":"⊆","subseteqq;":"⫅","subsetneq;":"⊊","subsetneqq;":"⫋","subsim;":"⫇","subsub;":"⫕","subsup;":"⫓","succ;":"≻","succapprox;":"⪸","succcurlyeq;":"≽","succeq;":"⪰","succnapprox;":"⪺","succneqq;":"⪶","succnsim;":"⋩","succsim;":"≿","sum;":"∑","sung;":"♪","sup1":"¹","sup1;":"¹","sup2":"²","sup2;":"²","sup3":"³","sup3;":"³","sup;":"⊃","supE;":"⫆","supdot;":"⪾","supdsub;":"⫘","supe;":"⊇","supedot;":"⫄","suphsol;":"⟉","suphsub;":"⫗","suplarr;":"⥻","supmult;":"⫂","supnE;":"⫌","supne;":"⊋","supplus;":"⫀","supset;":"⊃","supseteq;":"⊇","supseteqq;":"⫆","supsetneq;":"⊋","supsetneqq;":"⫌","supsim;":"⫈","supsub;":"⫔","supsup;":"⫖","swArr;":"⇙","swarhk;":"⤦","swarr;":"↙","swarrow;":"↙","swnwar;":"⤪","szlig":"ß","szlig;":"ß","target;":"⌖","tau;":"τ","tbrk;":"⎴","tcaron;":"ť","tcedil;":"ţ","tcy;":"т","tdot;":"⃛","telrec;":"⌕","tfr;":"𝔱","there4;":"∴","therefore;":"∴","theta;":"θ","thetasym;":"ϑ","thetav;":"ϑ","thickapprox;":"≈","thicksim;":"∼","thinsp;":" ","thkap;":"≈","thksim;":"∼","thorn":"þ","thorn;":"þ","tilde;":"˜","times":"×","times;":"×","timesb;":"⊠","timesbar;":"⨱","timesd;":"⨰","tint;":"∭","toea;":"⤨","top;":"⊤","topbot;":"⌶","topcir;":"⫱","topf;":"𝕥","topfork;":"⫚","tosa;":"⤩","tprime;":"‴","trade;":"™","triangle;":"▵","triangledown;":"▿","triangleleft;":"◃","trianglelefteq;":"⊴","triangleq;":"≜","triangleright;":"▹","trianglerighteq;":"⊵","tridot;":"◬","trie;":"≜","triminus;":"⨺","triplus;":"⨹","trisb;":"⧍","tritime;":"⨻","trpezium;":"⏢","tscr;":"𝓉","tscy;":"ц","tshcy;":"ћ","tstrok;":"ŧ","twixt;":"≬","twoheadleftarrow;":"↞","twoheadrightarrow;":"↠","uArr;":"⇑","uHar;":"⥣","uacute":"ú","uacute;":"ú","uarr;":"↑","ubrcy;":"ў","ubreve;":"ŭ","ucirc":"û","ucirc;":"û","ucy;":"у","udarr;":"⇅","udblac;":"ű","udhar;":"⥮","ufisht;":"⥾","ufr;":"𝔲","ugrave":"ù","ugrave;":"ù","uharl;":"↿","uharr;":"↾","uhblk;":"▀","ulcorn;":"⌜","ulcorner;":"⌜","ulcrop;":"⌏","ultri;":"◸","umacr;":"ū","uml":"¨","uml;":"¨","uogon;":"ų","uopf;":"𝕦","uparrow;":"↑","updownarrow;":"↕","upharpoonleft;":"↿","upharpoonright;":"↾","uplus;":"⊎","upsi;":"υ","upsih;":"ϒ","upsilon;":"υ","upuparrows;":"⇈","urcorn;":"⌝","urcorner;":"⌝","urcrop;":"⌎","uring;":"ů","urtri;":"◹","uscr;":"𝓊","utdot;":"⋰","utilde;":"ũ","utri;":"▵","utrif;":"▴","uuarr;":"⇈","uuml":"ü","uuml;":"ü","uwangle;":"⦧","vArr;":"⇕","vBar;":"⫨","vBarv;":"⫩","vDash;":"⊨","vangrt;":"⦜","varepsilon;":"ϵ","varkappa;":"ϰ","varnothing;":"∅","varphi;":"ϕ","varpi;":"ϖ","varpropto;":"∝","varr;":"↕","varrho;":"ϱ","varsigma;":"ς","varsubsetneq;":"⊊︀","varsubsetneqq;":"⫋︀","varsupsetneq;":"⊋︀","varsupsetneqq;":"⫌︀","vartheta;":"ϑ","vartriangleleft;":"⊲","vartriangleright;":"⊳","vcy;":"в","vdash;":"⊢","vee;":"∨","veebar;":"⊻","veeeq;":"≚","vellip;":"⋮","verbar;":"|","vert;":"|","vfr;":"𝔳","vltri;":"⊲","vnsub;":"⊂⃒","vnsup;":"⊃⃒","vopf;":"𝕧","vprop;":"∝","vrtri;":"⊳","vscr;":"𝓋","vsubnE;":"⫋︀","vsubne;":"⊊︀","vsupnE;":"⫌︀","vsupne;":"⊋︀","vzigzag;":"⦚","wcirc;":"ŵ","wedbar;":"⩟","wedge;":"∧","wedgeq;":"≙","weierp;":"℘","wfr;":"𝔴","wopf;":"𝕨","wp;":"℘","wr;":"≀","wreath;":"≀","wscr;":"𝓌","xcap;":"⋂","xcirc;":"◯","xcup;":"⋃","xdtri;":"▽","xfr;":"𝔵","xhArr;":"⟺","xharr;":"⟷","xi;":"ξ","xlArr;":"⟸","xlarr;":"⟵","xmap;":"⟼","xnis;":"⋻","xodot;":"⨀","xopf;":"𝕩","xoplus;":"⨁","xotime;":"⨂","xrArr;":"⟹","xrarr;":"⟶","xscr;":"𝓍","xsqcup;":"⨆","xuplus;":"⨄","xutri;":"△","xvee;":"⋁","xwedge;":"⋀","yacute":"ý","yacute;":"ý","yacy;":"я","ycirc;":"ŷ","ycy;":"ы","yen":"¥","yen;":"¥","yfr;":"𝔶","yicy;":"ї","yopf;":"𝕪","yscr;":"𝓎","yucy;":"ю","yuml":"ÿ","yuml;":"ÿ","zacute;":"ź","zcaron;":"ž","zcy;":"з","zdot;":"ż","zeetrf;":"ℨ","zeta;":"ζ","zfr;":"𝔷","zhcy;":"ж","zigrarr;":"⇝","zopf;":"𝕫","zscr;":"𝓏","zwj;":"‍","zwnj;":"‌"})));
  159. // #endregion
  160. const STATE_DATA = 0;
  161. const STATE_TAG_OPEN = 1;
  162. const STATE_END_TAG_OPEN = 2;
  163. const STATE_TAG_NAME = 3;
  164. const STATE_BEFORE_ATTRIBUTE_NAME = 4;
  165. const STATE_ATTRIBUTE_NAME = 5;
  166. const STATE_AFTER_ATTRIBUTE_NAME = 6;
  167. const STATE_BEFORE_ATTRIBUTE_VALUE = 7;
  168. const STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED = 8;
  169. const STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED = 9;
  170. const STATE_ATTRIBUTE_VALUE_UNQUOTED = 10;
  171. const STATE_AFTER_ATTRIBUTE_VALUE_QUOTED = 11;
  172. const STATE_SELF_CLOSING_START_TAG = 12;
  173. const STATE_MARKUP_DECLARATION_OPEN = 13;
  174. const STATE_COMMENT_START = 14;
  175. const STATE_COMMENT_START_DASH = 15;
  176. const STATE_COMMENT = 16;
  177. const STATE_COMMENT_END_DASH = 17;
  178. const STATE_COMMENT_END = 18;
  179. const STATE_COMMENT_END_BANG = 19;
  180. const STATE_BOGUS_COMMENT = 20;
  181. const STATE_COMMENT_LESS_THAN_SIGN = 21;
  182. const STATE_COMMENT_LESS_THAN_SIGN_BANG = 22;
  183. const STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH = 23;
  184. const STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH_DASH = 24;
  185. const STATE_DOCTYPE = 25;
  186. const STATE_BEFORE_DOCTYPE_NAME = 26;
  187. const STATE_DOCTYPE_NAME = 27;
  188. const STATE_AFTER_DOCTYPE_NAME = 28;
  189. const STATE_AFTER_DOCTYPE_PUBLIC_KEYWORD = 29;
  190. const STATE_BEFORE_DOCTYPE_PUBLIC_IDENTIFIER = 30;
  191. const STATE_DOCTYPE_PUBLIC_IDENTIFIER_DOUBLE_QUOTED = 31;
  192. const STATE_DOCTYPE_PUBLIC_IDENTIFIER_SINGLE_QUOTED = 32;
  193. const STATE_AFTER_DOCTYPE_PUBLIC_IDENTIFIER = 33;
  194. const STATE_BETWEEN_DOCTYPE_PUBLIC_AND_SYSTEM_IDENTIFIERS = 34;
  195. const STATE_AFTER_DOCTYPE_SYSTEM_KEYWORD = 35;
  196. const STATE_BEFORE_DOCTYPE_SYSTEM_IDENTIFIER = 36;
  197. const STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED = 37;
  198. const STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED = 38;
  199. const STATE_AFTER_DOCTYPE_SYSTEM_IDENTIFIER = 39;
  200. const STATE_BOGUS_DOCTYPE = 40;
  201. const STATE_CDATA_SECTION = 41;
  202. const STATE_CDATA_SECTION_BRACKET = 42;
  203. const STATE_CDATA_SECTION_END = 43;
  204. const STATE_RCDATA = 44;
  205. const STATE_RCDATA_LESS_THAN_SIGN = 45;
  206. const STATE_RCDATA_END_TAG_OPEN = 46;
  207. const STATE_RCDATA_END_TAG_NAME = 47;
  208. const STATE_RAWTEXT = 48;
  209. const STATE_RAWTEXT_LESS_THAN_SIGN = 49;
  210. const STATE_RAWTEXT_END_TAG_OPEN = 50;
  211. const STATE_RAWTEXT_END_TAG_NAME = 51;
  212. const STATE_SCRIPT_DATA = 52;
  213. const STATE_SCRIPT_DATA_LESS_THAN_SIGN = 53;
  214. const STATE_SCRIPT_DATA_END_TAG_OPEN = 54;
  215. const STATE_SCRIPT_DATA_END_TAG_NAME = 55;
  216. const STATE_SCRIPT_DATA_ESCAPE_START = 56;
  217. const STATE_SCRIPT_DATA_ESCAPE_START_DASH = 57;
  218. const STATE_SCRIPT_DATA_ESCAPED = 58;
  219. const STATE_SCRIPT_DATA_ESCAPED_DASH = 59;
  220. const STATE_SCRIPT_DATA_ESCAPED_DASH_DASH = 60;
  221. const STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN = 61;
  222. const STATE_SCRIPT_DATA_ESCAPED_END_TAG_OPEN = 62;
  223. const STATE_SCRIPT_DATA_ESCAPED_END_TAG_NAME = 63;
  224. const STATE_SCRIPT_DATA_DOUBLE_ESCAPE_START = 64;
  225. const STATE_SCRIPT_DATA_DOUBLE_ESCAPED = 65;
  226. const STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH = 66;
  227. const STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH_DASH = 67;
  228. const STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN = 68;
  229. const STATE_SCRIPT_DATA_DOUBLE_ESCAPE_END = 69;
  230. const STATE_PLAINTEXT = 70;
  231. // https://html.spec.whatwg.org/multipage/parsing.html#character-reference-state
  232. const STATE_CHARACTER_REFERENCE = 71;
  233. // https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
  234. const STATE_NAMED_CHARACTER_REFERENCE = 72;
  235. // https://html.spec.whatwg.org/multipage/parsing.html#ambiguous-ampersand-state
  236. const STATE_AMBIGUOUS_AMPERSAND = 73;
  237. // https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-state
  238. const STATE_NUMERIC_CHARACTER_REFERENCE = 74;
  239. // https://html.spec.whatwg.org/multipage/parsing.html#hexadecimal-character-reference-start-state
  240. const STATE_HEXADECIMAL_CHARACTER_REFERENCE_START = 75;
  241. // https://html.spec.whatwg.org/multipage/parsing.html#decimal-character-reference-start-state
  242. const STATE_DECIMAL_CHARACTER_REFERENCE_START = 76;
  243. // https://html.spec.whatwg.org/multipage/parsing.html#hexadecimal-character-reference-state
  244. const STATE_HEXADECIMAL_CHARACTER_REFERENCE = 77;
  245. // https://html.spec.whatwg.org/multipage/parsing.html#decimal-character-reference-state
  246. const STATE_DECIMAL_CHARACTER_REFERENCE = 78;
  247. // https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
  248. const STATE_NUMERIC_CHARACTER_REFERENCE_END = 79;
  249. const CC_TAB = 0x09;
  250. const CC_LF = 0x0a;
  251. const CC_FF = 0x0c;
  252. const CC_CR = 0x0d;
  253. const CC_SPACE = 0x20;
  254. const CC_NULL = 0x00;
  255. const CC_EXCLAMATION_MARK = 0x21;
  256. const CC_QUOTATION_MARK = 0x22;
  257. const CC_NUMBER_SIGN = 0x23;
  258. const CC_AMPERSAND = 0x26;
  259. const CC_APOSTROPHE = 0x27;
  260. const CC_HYPHEN_MINUS = 0x2d;
  261. const CC_SOLIDUS = 0x2f;
  262. const CC_SEMICOLON = 0x3b;
  263. const CC_LESS_THAN = 0x3c;
  264. const CC_EQUALS = 0x3d;
  265. const CC_GREATER_THAN = 0x3e;
  266. const CC_QUESTION_MARK = 0x3f;
  267. const CC_LEFT_SQUARE_BRACKET = 0x5b;
  268. const CC_RIGHT_SQUARE_BRACKET = 0x5d;
  269. const CC_LOW_LINE = 0x5f;
  270. const CC_GRAVE_ACCENT = 0x60;
  271. const CC_NO_BREAK_SPACE = 0xa0;
  272. const QUOTE_DOUBLE = 1;
  273. const QUOTE_SINGLE = 2;
  274. const QUOTE_NONE = 0;
  275. // Longest WHATWG named entity name *including* the trailing `;` is 32 chars
  276. // (`CounterClockwiseContourIntegral;`); without the trailing `;` it's 31.
  277. // Used to cap both the tokenizer's named-character-reference run length and
  278. // the decoder's longest-prefix backtrack so pathological inputs (e.g. `&`
  279. // followed by thousands of alphanumerics) stay linear-time.
  280. const MAX_ENTITY_NAME_LEN = 32;
  281. // ASCII character-class bit flags packed into one lookup table. The tokenizer
  282. // runs these predicates per code point (tag names, attribute names, character
  283. // references, whitespace skipping), so a single table load + mask replaces the
  284. // per-call comparison chains. Code points >= 0x80 are never in any of these
  285. // classes, so callers short-circuit on `cc < 0x80` before indexing.
  286. const CCLS_SPACE = 1;
  287. const CCLS_DIGIT = 2;
  288. const CCLS_UPPER = 4;
  289. const CCLS_LOWER = 8;
  290. const CCLS_HEX = 16;
  291. // Terminator sets of the tag-name / attribute-name states' fast-forward scans,
  292. // mirroring those states' arcs; one mask test replaces their compare chains.
  293. const CCLS_TAG_NAME_TERM = 32;
  294. const CCLS_ATTR_NAME_TERM = 64;
  295. const CCLS_ALPHA = CCLS_UPPER | CCLS_LOWER;
  296. const CCLS_ALNUM = CCLS_ALPHA | CCLS_DIGIT;
  297. const CHAR_CLASS = new Uint8Array(128);
  298. for (let i = 0; i < 128; i++) {
  299. let f = 0;
  300. if (
  301. i === CC_TAB ||
  302. i === CC_LF ||
  303. i === CC_FF ||
  304. i === CC_CR ||
  305. i === CC_SPACE
  306. ) {
  307. f |= CCLS_SPACE;
  308. }
  309. if (i >= 0x30 && i <= 0x39) f |= CCLS_DIGIT | CCLS_HEX;
  310. if (i >= 0x41 && i <= 0x5a) f |= CCLS_UPPER;
  311. if (i >= 0x61 && i <= 0x7a) f |= CCLS_LOWER;
  312. if ((i >= 0x41 && i <= 0x46) || (i >= 0x61 && i <= 0x66)) f |= CCLS_HEX;
  313. if (
  314. (f & CCLS_SPACE) !== 0 ||
  315. i === CC_SOLIDUS ||
  316. i === CC_GREATER_THAN ||
  317. i === CC_NULL
  318. ) {
  319. f |= CCLS_TAG_NAME_TERM | CCLS_ATTR_NAME_TERM;
  320. }
  321. if (
  322. i === CC_EQUALS ||
  323. i === CC_QUOTATION_MARK ||
  324. i === CC_APOSTROPHE ||
  325. i === CC_LESS_THAN
  326. ) {
  327. f |= CCLS_ATTR_NAME_TERM;
  328. }
  329. CHAR_CLASS[i] = f;
  330. }
  331. /**
  332. * @param {number} cc character code
  333. * @returns {boolean} is ascii alpha
  334. */
  335. const isAsciiAlpha = (cc) => cc < 0x80 && (CHAR_CLASS[cc] & CCLS_ALPHA) !== 0;
  336. /**
  337. * @param {number} cc character code
  338. * @returns {boolean} is ascii alphanumeric
  339. */
  340. const isAsciiAlphanumeric = (cc) =>
  341. cc < 0x80 && (CHAR_CLASS[cc] & CCLS_ALNUM) !== 0;
  342. /**
  343. * @param {number} cc character code
  344. * @returns {boolean} is ascii digit
  345. */
  346. const isAsciiDigit = (cc) => cc < 0x80 && (CHAR_CLASS[cc] & CCLS_DIGIT) !== 0;
  347. /**
  348. * @param {number} cc character code
  349. * @returns {boolean} is ascii hex digit
  350. */
  351. const isAsciiHexDigit = (cc) => cc < 0x80 && (CHAR_CLASS[cc] & CCLS_HEX) !== 0;
  352. /**
  353. * @param {number} cc character code
  354. * @returns {boolean} is ascii upper alpha
  355. */
  356. const isAsciiUpperAlpha = (cc) =>
  357. cc < 0x80 && (CHAR_CLASS[cc] & CCLS_UPPER) !== 0;
  358. /**
  359. * @param {number} cc character code
  360. * @returns {boolean} is ascii lower alpha
  361. */
  362. const isAsciiLowerAlpha = (cc) =>
  363. cc < 0x80 && (CHAR_CLASS[cc] & CCLS_LOWER) !== 0;
  364. /**
  365. * Tokenizer whitespace. U+000D CARRIAGE RETURN is included because the spec's
  366. * input-stream preprocessing converts CR (and CRLF) to LF before tokenizing;
  367. * this scanner keeps the original offsets, so it treats a raw CR as whitespace
  368. * to match the post-preprocessing behaviour.
  369. * @param {number} cc character code
  370. * @returns {boolean} is space
  371. */
  372. const isSpace = (cc) => cc < 0x80 && (CHAR_CLASS[cc] & CCLS_SPACE) !== 0;
  373. /**
  374. * @param {number} code numeric character reference code point
  375. * @returns {boolean} is a Unicode noncharacter
  376. */
  377. const isNoncharacter = (code) =>
  378. (code >= 0xfdd0 && code <= 0xfdef) || (code & 0xfffe) === 0xfffe;
  379. /**
  380. * @param {number} code numeric character reference code point
  381. * @returns {boolean} is a C0/C1 control that is not ASCII whitespace
  382. */
  383. const isControlReference = (code) =>
  384. code === 0x0d ||
  385. ((code <= 0x1f || (code >= 0x7f && code <= 0x9f)) &&
  386. code !== CC_TAB &&
  387. code !== CC_LF &&
  388. code !== CC_FF &&
  389. code !== CC_SPACE);
  390. /**
  391. * Severity of a tokenizer-detected parse error. `"warning"` is recoverable
  392. * (the tokenizer continued and the emitted token is still well-formed, e.g.
  393. * missing-attribute-value); `"error"` means the emitted token's offset
  394. * range is incomplete or does not match what the spec would produce, e.g.
  395. * eof-in-tag.
  396. *
  397. * Token offsets are JS string indices (UTF-16 code-unit offsets into
  398. * `input`), not byte offsets — relevant for inputs containing non-BMP
  399. * code points where one code point spans two indices.
  400. * @typedef {"warning" | "error"} ParseErrorSeverity
  401. */
  402. /**
  403. * @typedef {object} HtmlTokenCallbacks
  404. * @property {(input: string, start: number, end: number, nameStart: number, nameEnd: number, selfClosing: boolean) => number=} openTag
  405. * @property {(input: string, start: number, end: number, nameStart: number, nameEnd: number) => number=} closeTag
  406. * @property {(input: string, start: number, end: number) => number=} text
  407. * @property {(input: string, nameStart: number, nameEnd: number, valueStart: number, valueEnd: number, quoteType: number) => number=} attribute
  408. * @property {(input: string, start: number, end: number, dataStart: number, dataEnd: number) => number=} comment
  409. * @property {(input: string, start: number, end: number, nameStart: number, nameEnd: number, publicStart: number, publicEnd: number, systemStart: number, systemEnd: number, forceQuirks: boolean) => number=} doctype
  410. * @property {(input: string, code: string, start: number, end: number, severity: ParseErrorSeverity) => void=} parseError
  411. * @property {(() => boolean)=} isForeign returns true when the adjusted current node is in a foreign (SVG/MathML) namespace, which is where a `<![CDATA[` opens a CDATA section
  412. * @property {((name: string) => boolean)=} vetoesContentMode returns true when the tree builder will not run the algorithm that switches the tokenizer for this start tag, so it stays in the data state
  413. * @property {string=} fragmentContext context element tag name for fragment parsing; seeds the initial content mode
  414. */
  415. /**
  416. * @param {string} name tag name (lowercase)
  417. * @returns {number} content mode state for this tag, or STATE_DATA
  418. */
  419. const getContentModeForTag = (name) => {
  420. switch (name) {
  421. case "textarea":
  422. case "title":
  423. return STATE_RCDATA;
  424. case "style":
  425. case "xmp":
  426. case "iframe":
  427. case "noembed":
  428. case "noframes":
  429. return STATE_RAWTEXT;
  430. case "script":
  431. return STATE_SCRIPT_DATA;
  432. case "plaintext":
  433. return STATE_PLAINTEXT;
  434. default:
  435. return STATE_DATA;
  436. }
  437. };
  438. /**
  439. * Case-insensitive comparison of `input[start..end)` to a lowercase ASCII
  440. * literal, without allocating the slice.
  441. * @param {string} input input
  442. * @param {number} start range start
  443. * @param {number} end range end
  444. * @param {string} lit lowercase ASCII literal
  445. * @returns {boolean} true if the range equals `lit` ignoring ASCII case
  446. */
  447. const rangeEqualsLowerCase = (input, start, end, lit) => {
  448. if (end - start !== lit.length) return false;
  449. for (let i = 0; i < lit.length; i++) {
  450. let c = input.charCodeAt(start + i);
  451. if (c >= 0x41 && c <= 0x5a) c += 0x20;
  452. if (c !== lit.charCodeAt(i)) return false;
  453. }
  454. return true;
  455. };
  456. /**
  457. * Content mode for the just-opened tag whose name spans `input[start..end)`,
  458. * matched on the raw range so ordinary tags need neither a slice nor a
  459. * `toLowerCase`. Mirrors `getContentModeForTag`.
  460. * @param {string} input input
  461. * @param {number} start tag-name start
  462. * @param {number} end tag-name end
  463. * @returns {number} content mode state, or STATE_DATA
  464. */
  465. const getContentModeForRange = (input, start, end) => {
  466. switch (end - start) {
  467. case 3:
  468. if (rangeEqualsLowerCase(input, start, end, "xmp")) return STATE_RAWTEXT;
  469. return STATE_DATA;
  470. case 5:
  471. if (rangeEqualsLowerCase(input, start, end, "title")) return STATE_RCDATA;
  472. if (rangeEqualsLowerCase(input, start, end, "style")) {
  473. return STATE_RAWTEXT;
  474. }
  475. return STATE_DATA;
  476. case 6:
  477. if (rangeEqualsLowerCase(input, start, end, "script")) {
  478. return STATE_SCRIPT_DATA;
  479. }
  480. if (rangeEqualsLowerCase(input, start, end, "iframe")) {
  481. return STATE_RAWTEXT;
  482. }
  483. return STATE_DATA;
  484. case 7:
  485. if (rangeEqualsLowerCase(input, start, end, "noembed")) {
  486. return STATE_RAWTEXT;
  487. }
  488. return STATE_DATA;
  489. case 8:
  490. if (rangeEqualsLowerCase(input, start, end, "textarea")) {
  491. return STATE_RCDATA;
  492. }
  493. if (rangeEqualsLowerCase(input, start, end, "noframes")) {
  494. return STATE_RAWTEXT;
  495. }
  496. return STATE_DATA;
  497. case 9:
  498. if (rangeEqualsLowerCase(input, start, end, "plaintext")) {
  499. return STATE_PLAINTEXT;
  500. }
  501. return STATE_DATA;
  502. default:
  503. return STATE_DATA;
  504. }
  505. };
  506. /**
  507. * @param {string} input input string
  508. * @param {number} pos current position
  509. * @param {HtmlTokenCallbacks} callbacks callbacks
  510. * @returns {number} final position
  511. */
  512. const tokenize = (input, pos = 0, callbacks = {}) => {
  513. const len = input.length;
  514. let state = STATE_DATA;
  515. let returnState = STATE_DATA;
  516. let textStart = pos;
  517. let tagStart = pos;
  518. let tagNameStart = -1;
  519. let tagNameEnd = -1;
  520. let attributeNameStart = -1;
  521. let attributeNameEnd = -1;
  522. let attributeValueStart = -1;
  523. let attrQuoteType = QUOTE_NONE;
  524. let commentStart = pos;
  525. // Data range of the comment being scanned. The comment states append
  526. // characters that are not the ones just consumed (the `--` a `-->` inside a
  527. // comment turns into, the `--!` of a comment-end-bang), so the range is
  528. // tracked here rather than re-derived by trimming delimiters off the token.
  529. let commentDataStart = pos;
  530. let commentDataEnd = pos;
  531. // Sub-ranges of the DOCTYPE token being scanned (-1 = the spec's
  532. // "missing" value, which a consumer must tell apart from an empty one).
  533. let doctypeNameStart = -1;
  534. let doctypeNameEnd = -1;
  535. let doctypePublicStart = -1;
  536. let doctypePublicEnd = -1;
  537. let doctypeSystemStart = -1;
  538. let doctypeSystemEnd = -1;
  539. let doctypeForceQuirks = false;
  540. let lastOpenTagName = "";
  541. // Tag-name offsets of the last open tag; the lowercased `lastOpenTagName` is
  542. // derived from these lazily (only for special-content tags).
  543. let lastOpenTagStart = -1;
  544. let lastOpenTagEnd = -1;
  545. // Counter used by SCRIPT_DATA_DOUBLE_ESCAPE_{START,END} to detect whether
  546. // the ASCII-alpha run after `<` / `</` spells exactly `"script"`. Values
  547. // 0..6 = number of chars matched so far; 7 = no longer matches (sentinel).
  548. // Avoids growing a buffer for pathological inputs with long alpha runs.
  549. let scriptMatch = 0;
  550. let namedEntityConsumed = 0;
  551. // Offset of the opening `&` and the running numeric value (clamped past the
  552. // Unicode range so it can't overflow); used for numeric-reference errors.
  553. let charRefStart = -1;
  554. let charRefCode = 0;
  555. // Tracks whether the current tag has parsed any attributes — used to
  556. // fire the `end-tag-with-attributes` parse error when an end tag emits.
  557. let tagHasAttributes = false;
  558. // Memoized next occurrence of `<` / `&` / NUL for the text-run
  559. // fast-forwards: native `indexOf` beats a per-char JS loop, and `pos` only
  560. // moves forward, so each memo is refreshed at most once per occurrence
  561. // (`len` = no further occurrence).
  562. let nextLt = -1;
  563. let nextAmp = -1;
  564. let nextNul = -1;
  565. let nextHyphen = -1;
  566. let nextGt = -1;
  567. // Same memo scheme for the closing quote of quoted attribute values (long
  568. // values: data: URIs, srcset, inline style). Memoized so a value with many
  569. // `&` references doesn't re-run the quote scan per reference.
  570. let nextDQuote = -1;
  571. let nextSQuote = -1;
  572. /**
  573. * Reports a tokenizer parse error to the consumer. The offset range and
  574. * severity follow the WHATWG spec naming. Severity is `"error"` for
  575. * cases where the emitted token is incomplete (EOF inside a tag or
  576. * comment); everything else is a `"warning"`. Offsets are JS string
  577. * indices (UTF-16 code-unit offsets into `input`).
  578. * @param {string} code WHATWG parse-error code (kebab-case)
  579. * @param {number} start string offset where the error starts
  580. * @param {number} end string offset where the error ends
  581. * @param {ParseErrorSeverity} severity error severity
  582. */
  583. const reportError = (code, start, end, severity) => {
  584. if (callbacks.parseError !== undefined) {
  585. callbacks.parseError(input, code, start, end, severity);
  586. }
  587. };
  588. /**
  589. * Emits the WHATWG numeric-character-reference validation parse error for
  590. * the accumulated `charRefCode`, if any. Used both inline (when the
  591. * reference is terminated by a real next character) and at EOF (when the
  592. * reference runs to the end of input). The scanner only flags the error —
  593. * the spec's U+FFFD / Windows-1252 substitution is done by `decodeEntities`.
  594. * @param {number} endPos offset just past the reference
  595. */
  596. const validateNumericReference = (endPos) => {
  597. if (charRefCode === 0) {
  598. reportError("null-character-reference", charRefStart, endPos, "warning");
  599. } else if (charRefCode > 0x10ffff) {
  600. reportError(
  601. "character-reference-outside-unicode-range",
  602. charRefStart,
  603. endPos,
  604. "warning"
  605. );
  606. } else if (charRefCode >= 0xd800 && charRefCode <= 0xdfff) {
  607. reportError(
  608. "surrogate-character-reference",
  609. charRefStart,
  610. endPos,
  611. "warning"
  612. );
  613. } else if (isNoncharacter(charRefCode)) {
  614. reportError(
  615. "noncharacter-character-reference",
  616. charRefStart,
  617. endPos,
  618. "warning"
  619. );
  620. } else if (isControlReference(charRefCode)) {
  621. reportError(
  622. "control-character-reference",
  623. charRefStart,
  624. endPos,
  625. "warning"
  626. );
  627. }
  628. };
  629. // Content mode for the tag just opened (name at `lastOpenTagStart..End`). In
  630. // foreign content (SVG/MathML) the tree builder vetoes RAWTEXT/RCDATA/script
  631. // switching via `isForeign`, so e.g. an SVG `<title>`/`<style>` is parsed as
  632. // normal markup. `lastOpenTagName` (the lowercased name compared by the
  633. // special end-tag states) is materialized only when a special mode is
  634. // actually entered — ordinary tags never allocate it.
  635. const contentModeAfterOpenTag = () => {
  636. const m = getContentModeForRange(input, lastOpenTagStart, lastOpenTagEnd);
  637. // Ordinary tags stay in the data state whatever the tree builder did, so
  638. // they pay for neither the name nor the callback.
  639. if (m === STATE_DATA) return STATE_DATA;
  640. lastOpenTagName = input
  641. .slice(lastOpenTagStart, lastOpenTagEnd)
  642. .toLowerCase();
  643. if (
  644. callbacks.vetoesContentMode !== undefined &&
  645. callbacks.vetoesContentMode(lastOpenTagName)
  646. ) {
  647. return STATE_DATA;
  648. }
  649. return m;
  650. };
  651. // HTML fragment parsing: seed the tokenizer with the context element's
  652. // content mode (e.g. a `textarea`/`style`/`script` context starts in
  653. // RCDATA/RAWTEXT/script-data rather than data state).
  654. if (callbacks.fragmentContext !== undefined) {
  655. lastOpenTagName = callbacks.fragmentContext;
  656. state =
  657. callbacks.vetoesContentMode !== undefined &&
  658. callbacks.vetoesContentMode(lastOpenTagName)
  659. ? STATE_DATA
  660. : getContentModeForTag(lastOpenTagName);
  661. }
  662. /**
  663. * @param {number} endPos end position
  664. */
  665. const flushText = (endPos) => {
  666. if (textStart < endPos) {
  667. if (callbacks.text !== undefined) {
  668. callbacks.text(input, textStart, endPos);
  669. }
  670. // Advance `textStart` so a second `flushText` for the same span
  671. // (e.g. from the EOF handler after a tag-open transition already
  672. // flushed the pending text) is a no-op rather than a duplicate
  673. // emit. emitOpenTag / emitCloseTag overwrite `textStart` with
  674. // their own `nextPos` anyway, so this doesn't shift their start.
  675. textStart = endPos;
  676. }
  677. };
  678. /**
  679. * @param {number} endPos end position
  680. * @returns {number} next position
  681. */
  682. const emitAttribute = (endPos) => {
  683. // Default `nextPos` advances past the closing quote (if any) so the
  684. // state machine can continue when no `attribute` callback is provided.
  685. // When a callback IS provided, its return value overrides the default —
  686. // the callback is expected to do the same advance based on the
  687. // reported `quoteType`.
  688. let nextPos = attrQuoteType === QUOTE_NONE ? endPos : endPos + 1;
  689. if (callbacks.attribute !== undefined && attributeNameStart !== -1) {
  690. nextPos = callbacks.attribute(
  691. input,
  692. attributeNameStart,
  693. attributeNameEnd,
  694. attributeValueStart,
  695. attributeValueStart === -1 ? -1 : endPos,
  696. attrQuoteType
  697. );
  698. }
  699. if (attributeNameStart !== -1) tagHasAttributes = true;
  700. attributeNameStart = -1;
  701. attributeValueStart = -1;
  702. attrQuoteType = QUOTE_NONE;
  703. return nextPos;
  704. };
  705. /**
  706. * @param {number} endPos end position
  707. * @param {boolean} selfClosing is self closing
  708. * @returns {number} next position
  709. */
  710. const emitOpenTag = (endPos, selfClosing) => {
  711. let nextPos = endPos;
  712. if (callbacks.openTag !== undefined) {
  713. nextPos = callbacks.openTag(
  714. input,
  715. tagStart,
  716. endPos,
  717. tagNameStart,
  718. tagNameEnd,
  719. selfClosing
  720. );
  721. }
  722. if (!selfClosing) {
  723. // Record offsets only; `contentModeAfterOpenTag` lowercases lazily.
  724. lastOpenTagStart = tagNameStart;
  725. lastOpenTagEnd = tagNameEnd;
  726. }
  727. tagHasAttributes = false;
  728. textStart = nextPos;
  729. return nextPos;
  730. };
  731. /**
  732. * @param {number} endPos end position
  733. * @returns {number} next position
  734. */
  735. const emitCloseTag = (endPos) => {
  736. // Per WHATWG: an end tag emitted with attributes is a parse error.
  737. if (tagHasAttributes) {
  738. reportError("end-tag-with-attributes", tagStart, endPos, "warning");
  739. }
  740. let nextPos = endPos;
  741. if (callbacks.closeTag !== undefined) {
  742. nextPos = callbacks.closeTag(
  743. input,
  744. tagStart,
  745. endPos,
  746. tagNameStart,
  747. tagNameEnd
  748. );
  749. }
  750. tagHasAttributes = false;
  751. textStart = nextPos;
  752. return nextPos;
  753. };
  754. while (pos < len) {
  755. const cc = input.charCodeAt(pos);
  756. // All WHATWG tokenizer states handled. Deliberately omitted parse errors
  757. // (need state this offset scanner lacks): duplicate-attribute,
  758. // cdata-in-html-content, `*-in-input-stream`. Reference substitution is
  759. // left to `decodeEntities`.
  760. switch (state) {
  761. // https://html.spec.whatwg.org/multipage/parsing.html#data-state
  762. case STATE_DATA:
  763. // Consume the next input character:
  764. // U+003C LESS-THAN SIGN (<)
  765. // Set the return state to the data state. Switch to the tag open state.
  766. if (cc === CC_LESS_THAN) {
  767. tagStart = pos;
  768. state = STATE_TAG_OPEN;
  769. pos++;
  770. } else if (cc === CC_AMPERSAND) {
  771. // U+0026 AMPERSAND (&)
  772. // Set the return state to the data state. Switch to the
  773. // character reference state.
  774. returnState = STATE_DATA;
  775. state = STATE_CHARACTER_REFERENCE;
  776. pos++;
  777. } else if (cc === CC_NULL) {
  778. // U+0000 NULL: unexpected-null-character (the data state
  779. // emits the NULL as-is; the scanner only flags the error).
  780. reportError("unexpected-null-character", pos, pos + 1, "warning");
  781. pos++;
  782. } else {
  783. // Fast-forward over the run of ordinary text without re-entering
  784. // the per-state switch — batches only the data state's "Anything
  785. // else — emit the current input character" arc; the stops are
  786. // exactly the code points with their own arcs (`<`, `&`, NUL).
  787. pos++;
  788. if (nextLt < pos) {
  789. nextLt = input.indexOf("<", pos);
  790. if (nextLt === -1) nextLt = len;
  791. }
  792. if (nextAmp < pos) {
  793. nextAmp = input.indexOf("&", pos);
  794. if (nextAmp === -1) nextAmp = len;
  795. }
  796. if (nextNul < pos) {
  797. nextNul = input.indexOf("\0", pos);
  798. if (nextNul === -1) nextNul = len;
  799. }
  800. pos = Math.min(nextLt, nextAmp, nextNul);
  801. // Fused: the memo invariant guarantees `input[nextLt] === "<"`, so
  802. // take this state's `<` arc without another dispatch.
  803. if (pos === nextLt && pos < len) {
  804. tagStart = pos;
  805. state = STATE_TAG_OPEN;
  806. pos++;
  807. }
  808. }
  809. break;
  810. // https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
  811. case STATE_TAG_OPEN:
  812. // Consume the next input character:
  813. // U+002F SOLIDUS (/)
  814. // Switch to the end tag open state.
  815. if (cc === CC_SOLIDUS) {
  816. state = STATE_END_TAG_OPEN;
  817. pos++;
  818. // Fused: when the next char is alpha, take the end-tag-open state's
  819. // alpha arc (and its tag-name run scan) without another dispatch.
  820. if (pos < len && isAsciiAlpha(input.charCodeAt(pos))) {
  821. flushText(tagStart);
  822. tagNameStart = pos;
  823. state = STATE_TAG_NAME;
  824. pos++;
  825. while (pos < len) {
  826. const c2 = input.charCodeAt(pos);
  827. if (c2 < 0x80 && (CHAR_CLASS[c2] & CCLS_TAG_NAME_TERM) !== 0) {
  828. break;
  829. }
  830. pos++;
  831. }
  832. }
  833. } else if (cc === CC_EXCLAMATION_MARK) {
  834. // U+0021 EXCLAMATION MARK (!)
  835. // Switch to the markup declaration open state.
  836. flushText(tagStart);
  837. commentStart = tagStart;
  838. state = STATE_MARKUP_DECLARATION_OPEN;
  839. pos++;
  840. } else if (isAsciiAlpha(cc)) {
  841. // ASCII alpha
  842. // Create a new start tag token, set its tag name to the empty string.
  843. // Reconsume in the tag name state.
  844. flushText(tagStart);
  845. tagNameStart = pos;
  846. state = STATE_TAG_NAME;
  847. // Fused reconsume: the first char is alpha, so the tag-name state
  848. // always lands in its run scan — run it here without a dispatch.
  849. pos++;
  850. while (pos < len) {
  851. const c2 = input.charCodeAt(pos);
  852. if (c2 < 0x80 && (CHAR_CLASS[c2] & CCLS_TAG_NAME_TERM) !== 0) {
  853. break;
  854. }
  855. pos++;
  856. }
  857. } else if (cc === CC_QUESTION_MARK) {
  858. // U+003F QUESTION MARK (?)
  859. // This is an unexpected-question-mark-instead-of-tag-name parse error.
  860. // Create a comment token whose data is the empty string. Reconsume in the
  861. // bogus comment state.
  862. reportError(
  863. "unexpected-question-mark-instead-of-tag-name",
  864. pos,
  865. pos + 1,
  866. "warning"
  867. );
  868. flushText(tagStart);
  869. commentStart = tagStart;
  870. commentDataStart = pos;
  871. commentDataEnd = pos;
  872. state = STATE_BOGUS_COMMENT;
  873. // Reconsume — let the bogus-comment state consume the `?`
  874. // itself, matching the spec.
  875. } else {
  876. // Anything else
  877. // This is an invalid-first-character-of-tag-name parse error. Emit a U+003C
  878. // LESS-THAN SIGN character token. Reconsume in the data state.
  879. reportError(
  880. "invalid-first-character-of-tag-name",
  881. pos,
  882. pos + 1,
  883. "warning"
  884. );
  885. state = STATE_DATA;
  886. // Reconsume
  887. }
  888. break;
  889. // https://html.spec.whatwg.org/multipage/parsing.html#end-tag-open-state
  890. case STATE_END_TAG_OPEN:
  891. // Consume the next input character. The spec's ASCII-alpha arc (create
  892. // an end tag token, reconsume in the tag name state) is taken inline by
  893. // the `<` `/` arc above, the only way into this state, so `cc` is never
  894. // ASCII alpha here.
  895. if (cc === CC_GREATER_THAN) {
  896. // U+003E GREATER-THAN SIGN (>)
  897. // This is a missing-end-tag-name parse error. Switch to the data state.
  898. // No token is emitted, so `</>` is dropped rather than left in the
  899. // text run: a browser renders nothing for it, and keeping it would
  900. // resurface as visible text once the text node is re-escaped.
  901. reportError("missing-end-tag-name", pos, pos + 1, "warning");
  902. flushText(tagStart);
  903. state = STATE_DATA;
  904. pos++;
  905. textStart = pos;
  906. } else {
  907. // Anything else
  908. // This is an invalid-first-character-of-tag-name parse error. Create a
  909. // comment token whose data is the empty string. Reconsume in the bogus
  910. // comment state.
  911. reportError(
  912. "invalid-first-character-of-tag-name",
  913. pos,
  914. pos + 1,
  915. "warning"
  916. );
  917. flushText(tagStart);
  918. commentStart = tagStart;
  919. commentDataStart = pos;
  920. commentDataEnd = pos;
  921. state = STATE_BOGUS_COMMENT;
  922. // Reconsume — let bogus-comment consume this char itself.
  923. }
  924. break;
  925. // https://html.spec.whatwg.org/multipage/parsing.html#tag-name-state
  926. case STATE_TAG_NAME:
  927. // Consume the next input character:
  928. // U+0009 CHARACTER TABULATION (tab)
  929. // U+000A LINE FEED (LF)
  930. // U+000C FORM FEED (FF)
  931. // U+0020 SPACE
  932. // Switch to the before attribute name state.
  933. if (isSpace(cc)) {
  934. tagNameEnd = pos;
  935. state = STATE_BEFORE_ATTRIBUTE_NAME;
  936. pos++;
  937. } else if (cc === CC_SOLIDUS) {
  938. // U+002F SOLIDUS (/)
  939. // Switch to the self-closing start tag state.
  940. tagNameEnd = pos;
  941. state = STATE_SELF_CLOSING_START_TAG;
  942. pos++;
  943. } else if (cc === CC_GREATER_THAN) {
  944. // U+003E GREATER-THAN SIGN (>)
  945. // Switch to the data state. Emit the current tag token.
  946. tagNameEnd = pos;
  947. if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) {
  948. state = STATE_DATA;
  949. pos = emitCloseTag(pos + 1);
  950. } else {
  951. const nextPos = emitOpenTag(pos + 1, false);
  952. state = nextPos > pos + 1 ? STATE_DATA : contentModeAfterOpenTag();
  953. pos = nextPos;
  954. }
  955. } else {
  956. // U+0000 NULL: unexpected-null-character (append U+FFFD).
  957. if (cc === CC_NULL) {
  958. reportError("unexpected-null-character", pos, pos + 1, "warning");
  959. }
  960. // Fast-forward over the ordinary run of the tag name.
  961. pos++;
  962. while (pos < len) {
  963. const c2 = input.charCodeAt(pos);
  964. if (c2 < 0x80 && (CHAR_CLASS[c2] & CCLS_TAG_NAME_TERM) !== 0) {
  965. break;
  966. }
  967. pos++;
  968. }
  969. }
  970. break;
  971. // https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-name-state
  972. case STATE_BEFORE_ATTRIBUTE_NAME:
  973. // Consume the next input character:
  974. // U+0009 CHARACTER TABULATION (tab)
  975. // U+000A LINE FEED (LF)
  976. // U+000C FORM FEED (FF)
  977. // U+0020 SPACE
  978. // Ignore the character.
  979. // Reconsume so space is handled in BEFORE_ATTRIBUTE_NAME
  980. if (isSpace(cc)) {
  981. pos++;
  982. } else if (cc === CC_SOLIDUS || cc === CC_GREATER_THAN) {
  983. // U+002F SOLIDUS (/)
  984. // U+003E GREATER-THAN SIGN (>)
  985. // EOF
  986. // Reconsume in the after attribute name state.
  987. state = STATE_AFTER_ATTRIBUTE_NAME;
  988. // Reconsume
  989. } else if (cc === CC_EQUALS) {
  990. // U+003D EQUALS SIGN (=)
  991. // This is an unexpected-equals-sign-before-attribute-name parse
  992. // error. Start a new attribute. Switch to the attribute name state.
  993. reportError(
  994. "unexpected-equals-sign-before-attribute-name",
  995. pos,
  996. pos + 1,
  997. "warning"
  998. );
  999. attributeNameStart = pos;
  1000. state = STATE_ATTRIBUTE_NAME;
  1001. pos++;
  1002. } else {
  1003. // Anything else
  1004. // Start a new attribute in the current tag token. Set that attribute name
  1005. // and value to the empty string. Reconsume in the attribute name state.
  1006. attributeNameStart = pos;
  1007. state = STATE_ATTRIBUTE_NAME;
  1008. // Fused reconsume: `cc` can't be a terminator here (space / `/` /
  1009. // `>` / `=` took earlier arcs), so run the attribute-name state's
  1010. // else arc — its first-char errors and run scan — without a dispatch.
  1011. if (cc === CC_NULL) {
  1012. reportError("unexpected-null-character", pos, pos + 1, "warning");
  1013. } else if (
  1014. cc === CC_QUOTATION_MARK ||
  1015. cc === CC_APOSTROPHE ||
  1016. cc === CC_LESS_THAN
  1017. ) {
  1018. reportError(
  1019. "unexpected-character-in-attribute-name",
  1020. pos,
  1021. pos + 1,
  1022. "warning"
  1023. );
  1024. }
  1025. pos++;
  1026. while (pos < len) {
  1027. const c2 = input.charCodeAt(pos);
  1028. if (c2 < 0x80 && (CHAR_CLASS[c2] & CCLS_ATTR_NAME_TERM) !== 0) {
  1029. break;
  1030. }
  1031. pos++;
  1032. }
  1033. }
  1034. break;
  1035. // https://html.spec.whatwg.org/multipage/parsing.html#attribute-name-state
  1036. case STATE_ATTRIBUTE_NAME:
  1037. // Consume the next input character:
  1038. // U+0009 CHARACTER TABULATION (tab)
  1039. // U+000A LINE FEED (LF)
  1040. // U+000C FORM FEED (FF)
  1041. // U+0020 SPACE
  1042. // U+002F SOLIDUS (/)
  1043. // U+003E GREATER-THAN SIGN (>)
  1044. // EOF
  1045. // Reconsume in the after attribute name state.
  1046. if (isSpace(cc) || cc === CC_SOLIDUS || cc === CC_GREATER_THAN) {
  1047. attributeNameEnd = pos;
  1048. state = STATE_AFTER_ATTRIBUTE_NAME;
  1049. // Reconsume
  1050. } else if (cc === CC_EQUALS) {
  1051. attributeNameEnd = pos;
  1052. state = STATE_BEFORE_ATTRIBUTE_VALUE;
  1053. pos++;
  1054. } else {
  1055. // NULL -> unexpected-null-character; `"` `'` `<` -> unexpected-character-in-attribute-name.
  1056. if (cc === CC_NULL) {
  1057. reportError("unexpected-null-character", pos, pos + 1, "warning");
  1058. } else if (
  1059. cc === CC_QUOTATION_MARK ||
  1060. cc === CC_APOSTROPHE ||
  1061. cc === CC_LESS_THAN
  1062. ) {
  1063. reportError(
  1064. "unexpected-character-in-attribute-name",
  1065. pos,
  1066. pos + 1,
  1067. "warning"
  1068. );
  1069. }
  1070. // Fast-forward over the ordinary run of the attribute name; stop on
  1071. // any terminator (space / `/` / `>` / `=`) or a char that needs a
  1072. // per-occurrence parse error (NULL / `"` / `'` / `<`), which the
  1073. // outer switch then re-handles.
  1074. pos++;
  1075. while (pos < len) {
  1076. const c2 = input.charCodeAt(pos);
  1077. if (c2 < 0x80 && (CHAR_CLASS[c2] & CCLS_ATTR_NAME_TERM) !== 0) {
  1078. break;
  1079. }
  1080. pos++;
  1081. }
  1082. }
  1083. break;
  1084. // https://html.spec.whatwg.org/multipage/parsing.html#after-attribute-name-state
  1085. case STATE_AFTER_ATTRIBUTE_NAME:
  1086. // Consume the next input character:
  1087. // U+0009 CHARACTER TABULATION (tab)
  1088. // U+000A LINE FEED (LF)
  1089. // U+000C FORM FEED (FF)
  1090. // U+0020 SPACE
  1091. // Ignore the character.
  1092. if (isSpace(cc)) {
  1093. pos++;
  1094. } else if (cc === CC_SOLIDUS) {
  1095. // U+002F SOLIDUS (/)
  1096. // Switch to the self-closing start tag state.
  1097. emitAttribute(pos);
  1098. state = STATE_SELF_CLOSING_START_TAG;
  1099. pos++;
  1100. } else if (cc === CC_EQUALS) {
  1101. // U+003D EQUALS SIGN (=)
  1102. // Switch to the before attribute value state.
  1103. state = STATE_BEFORE_ATTRIBUTE_VALUE;
  1104. pos++;
  1105. } else if (cc === CC_GREATER_THAN) {
  1106. // U+003E GREATER-THAN SIGN (>)
  1107. // Switch to the data state. Emit the current tag token.
  1108. emitAttribute(pos);
  1109. if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) {
  1110. state = STATE_DATA;
  1111. pos = emitCloseTag(pos + 1);
  1112. } else {
  1113. const nextPos = emitOpenTag(pos + 1, false);
  1114. state = nextPos > pos + 1 ? STATE_DATA : contentModeAfterOpenTag();
  1115. pos = nextPos;
  1116. }
  1117. } else {
  1118. // Anything else
  1119. // Start a new attribute in the current tag token.
  1120. emitAttribute(pos);
  1121. attributeNameStart = pos;
  1122. state = STATE_ATTRIBUTE_NAME;
  1123. // Reconsume
  1124. }
  1125. break;
  1126. // https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-value-state
  1127. case STATE_BEFORE_ATTRIBUTE_VALUE:
  1128. // Consume the next input character:
  1129. // U+0009 CHARACTER TABULATION (tab)
  1130. // U+000A LINE FEED (LF)
  1131. // U+000C FORM FEED (FF)
  1132. // U+0020 SPACE
  1133. // Ignore the character.
  1134. if (isSpace(cc)) {
  1135. pos++;
  1136. } else if (cc === CC_QUOTATION_MARK) {
  1137. // U+0022 QUOTATION MARK (")
  1138. // Switch to the attribute value (double-quoted) state.
  1139. attributeValueStart = pos + 1;
  1140. attrQuoteType = QUOTE_DOUBLE;
  1141. state = STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED;
  1142. pos++;
  1143. // Fused: run that state's memoized value scan now; a leading `"` /
  1144. // `&` / NUL keeps the min at `pos` for the next dispatch.
  1145. if (nextDQuote < pos) {
  1146. nextDQuote = input.indexOf('"', pos);
  1147. if (nextDQuote === -1) nextDQuote = len;
  1148. }
  1149. if (nextAmp < pos) {
  1150. nextAmp = input.indexOf("&", pos);
  1151. if (nextAmp === -1) nextAmp = len;
  1152. }
  1153. if (nextNul < pos) {
  1154. nextNul = input.indexOf("\0", pos);
  1155. if (nextNul === -1) nextNul = len;
  1156. }
  1157. pos = Math.min(nextDQuote, nextAmp, nextNul);
  1158. } else if (cc === CC_APOSTROPHE) {
  1159. // U+0027 APOSTROPHE (')
  1160. // Switch to the attribute value (single-quoted) state.
  1161. attributeValueStart = pos + 1;
  1162. attrQuoteType = QUOTE_SINGLE;
  1163. state = STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED;
  1164. pos++;
  1165. // Fused: run that state's memoized value scan now; a leading `'` /
  1166. // `&` / NUL keeps the min at `pos` for the next dispatch.
  1167. if (nextSQuote < pos) {
  1168. nextSQuote = input.indexOf("'", pos);
  1169. if (nextSQuote === -1) nextSQuote = len;
  1170. }
  1171. if (nextAmp < pos) {
  1172. nextAmp = input.indexOf("&", pos);
  1173. if (nextAmp === -1) nextAmp = len;
  1174. }
  1175. if (nextNul < pos) {
  1176. nextNul = input.indexOf("\0", pos);
  1177. if (nextNul === -1) nextNul = len;
  1178. }
  1179. pos = Math.min(nextSQuote, nextAmp, nextNul);
  1180. } else if (cc === CC_GREATER_THAN) {
  1181. // U+003E GREATER-THAN SIGN (>)
  1182. // This is a missing-attribute-value parse error. Switch to the data
  1183. // state. Emit the current tag token. The attribute is reported with
  1184. // an empty value range pointing at the `>` so the open-tag offset range
  1185. // still includes the `>`.
  1186. reportError("missing-attribute-value", pos, pos + 1, "warning");
  1187. attributeValueStart = pos;
  1188. attrQuoteType = QUOTE_NONE;
  1189. pos = emitAttribute(pos);
  1190. if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) {
  1191. state = STATE_DATA;
  1192. pos = emitCloseTag(pos + 1);
  1193. } else {
  1194. const nextPos = emitOpenTag(pos + 1, false);
  1195. state = nextPos > pos + 1 ? STATE_DATA : contentModeAfterOpenTag();
  1196. pos = nextPos;
  1197. }
  1198. } else {
  1199. // Anything else
  1200. // Reconsume in the attribute value (unquoted) state.
  1201. attributeValueStart = pos;
  1202. attrQuoteType = QUOTE_NONE;
  1203. state = STATE_ATTRIBUTE_VALUE_UNQUOTED;
  1204. // Reconsume
  1205. }
  1206. break;
  1207. // https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(double-quoted)-state
  1208. case STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED:
  1209. // Consume the next input character:
  1210. // U+0022 QUOTATION MARK (")
  1211. // Switch to the after attribute value (quoted) state.
  1212. if (cc === CC_QUOTATION_MARK) {
  1213. pos = emitAttribute(pos);
  1214. state = STATE_AFTER_ATTRIBUTE_VALUE_QUOTED;
  1215. } else if (cc === CC_AMPERSAND) {
  1216. // U+0026 AMPERSAND (&)
  1217. // Set the return state to the attribute value (double-quoted)
  1218. // state. Switch to the character reference state.
  1219. returnState = STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED;
  1220. state = STATE_CHARACTER_REFERENCE;
  1221. pos++;
  1222. } else if (cc === CC_NULL) {
  1223. // U+0000 NULL: unexpected-null-character (append U+FFFD).
  1224. reportError("unexpected-null-character", pos, pos + 1, "warning");
  1225. pos++;
  1226. } else {
  1227. // Fast-forward over the ordinary run of the quoted value with the
  1228. // same memoized native-scan scheme as the data state.
  1229. pos++;
  1230. if (nextDQuote < pos) {
  1231. nextDQuote = input.indexOf('"', pos);
  1232. if (nextDQuote === -1) nextDQuote = len;
  1233. }
  1234. if (nextAmp < pos) {
  1235. nextAmp = input.indexOf("&", pos);
  1236. if (nextAmp === -1) nextAmp = len;
  1237. }
  1238. if (nextNul < pos) {
  1239. nextNul = input.indexOf("\0", pos);
  1240. if (nextNul === -1) nextNul = len;
  1241. }
  1242. pos = Math.min(nextDQuote, nextAmp, nextNul);
  1243. }
  1244. break;
  1245. // https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(single-quoted)-state
  1246. case STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED:
  1247. // Consume the next input character:
  1248. // U+0027 APOSTROPHE (')
  1249. // Switch to the after attribute value (quoted) state.
  1250. if (cc === CC_APOSTROPHE) {
  1251. pos = emitAttribute(pos);
  1252. state = STATE_AFTER_ATTRIBUTE_VALUE_QUOTED;
  1253. } else if (cc === CC_AMPERSAND) {
  1254. // U+0026 AMPERSAND (&)
  1255. // Set the return state to the attribute value (single-quoted)
  1256. // state. Switch to the character reference state.
  1257. returnState = STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED;
  1258. state = STATE_CHARACTER_REFERENCE;
  1259. pos++;
  1260. } else if (cc === CC_NULL) {
  1261. // U+0000 NULL: unexpected-null-character (append U+FFFD).
  1262. reportError("unexpected-null-character", pos, pos + 1, "warning");
  1263. pos++;
  1264. } else {
  1265. // Fast-forward over the ordinary run of the quoted value with the
  1266. // same memoized native-scan scheme as the data state.
  1267. pos++;
  1268. if (nextSQuote < pos) {
  1269. nextSQuote = input.indexOf("'", pos);
  1270. if (nextSQuote === -1) nextSQuote = len;
  1271. }
  1272. if (nextAmp < pos) {
  1273. nextAmp = input.indexOf("&", pos);
  1274. if (nextAmp === -1) nextAmp = len;
  1275. }
  1276. if (nextNul < pos) {
  1277. nextNul = input.indexOf("\0", pos);
  1278. if (nextNul === -1) nextNul = len;
  1279. }
  1280. pos = Math.min(nextSQuote, nextAmp, nextNul);
  1281. }
  1282. break;
  1283. // https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(unquoted)-state
  1284. case STATE_ATTRIBUTE_VALUE_UNQUOTED:
  1285. if (isSpace(cc)) {
  1286. pos = emitAttribute(pos);
  1287. state = STATE_BEFORE_ATTRIBUTE_NAME;
  1288. // Reconsume so space is handled in BEFORE_ATTRIBUTE_NAME
  1289. } else if (cc === CC_GREATER_THAN) {
  1290. // U+003E GREATER-THAN SIGN (>)
  1291. // This is a missing-attribute-value parse error. Switch to the data state.
  1292. // Emit the current tag token.
  1293. pos = emitAttribute(pos);
  1294. if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) {
  1295. state = STATE_DATA;
  1296. pos = emitCloseTag(pos + 1);
  1297. } else {
  1298. const nextPos = emitOpenTag(pos + 1, false);
  1299. state = nextPos > pos + 1 ? STATE_DATA : contentModeAfterOpenTag();
  1300. pos = nextPos;
  1301. }
  1302. } else if (cc === CC_AMPERSAND) {
  1303. // U+0026 AMPERSAND (&)
  1304. // Set the return state to the attribute value (unquoted)
  1305. // state. Switch to the character reference state.
  1306. returnState = STATE_ATTRIBUTE_VALUE_UNQUOTED;
  1307. state = STATE_CHARACTER_REFERENCE;
  1308. pos++;
  1309. } else {
  1310. // NULL -> unexpected-null-character; `"` `'` `<` `=` `` ` `` -> unexpected-character-in-unquoted-attribute-value.
  1311. if (cc === CC_NULL) {
  1312. reportError("unexpected-null-character", pos, pos + 1, "warning");
  1313. } else if (
  1314. cc === CC_QUOTATION_MARK ||
  1315. cc === CC_APOSTROPHE ||
  1316. cc === CC_LESS_THAN ||
  1317. cc === CC_EQUALS ||
  1318. cc === CC_GRAVE_ACCENT
  1319. ) {
  1320. reportError(
  1321. "unexpected-character-in-unquoted-attribute-value",
  1322. pos,
  1323. pos + 1,
  1324. "warning"
  1325. );
  1326. }
  1327. pos++;
  1328. }
  1329. break;
  1330. // https://html.spec.whatwg.org/multipage/parsing.html#after-attribute-value-(quoted)-state
  1331. case STATE_AFTER_ATTRIBUTE_VALUE_QUOTED:
  1332. // Consume the next input character:
  1333. // U+0009 CHARACTER TABULATION (tab)
  1334. // U+000A LINE FEED (LF)
  1335. // U+000C FORM FEED (FF)
  1336. // U+0020 SPACE
  1337. // Switch to the before attribute name state.
  1338. if (isSpace(cc)) {
  1339. state = STATE_BEFORE_ATTRIBUTE_NAME;
  1340. pos++;
  1341. } else if (cc === CC_SOLIDUS) {
  1342. // U+002F SOLIDUS (/)
  1343. // Switch to the self-closing start tag state.
  1344. state = STATE_SELF_CLOSING_START_TAG;
  1345. pos++;
  1346. } else if (cc === CC_GREATER_THAN) {
  1347. if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) {
  1348. state = STATE_DATA;
  1349. pos = emitCloseTag(pos + 1);
  1350. } else {
  1351. const nextPos = emitOpenTag(pos + 1, false);
  1352. state = nextPos > pos + 1 ? STATE_DATA : contentModeAfterOpenTag();
  1353. pos = nextPos;
  1354. }
  1355. } else {
  1356. // Anything else
  1357. // This is a missing-whitespace-between-attributes parse error. Reconsume in
  1358. // the before attribute name state.
  1359. reportError(
  1360. "missing-whitespace-between-attributes",
  1361. pos,
  1362. pos + 1,
  1363. "warning"
  1364. );
  1365. state = STATE_BEFORE_ATTRIBUTE_NAME;
  1366. // Reconsume
  1367. }
  1368. break;
  1369. // https://html.spec.whatwg.org/multipage/parsing.html#self-closing-start-tag-state
  1370. case STATE_SELF_CLOSING_START_TAG:
  1371. // Consume the next input character:
  1372. // U+003E GREATER-THAN SIGN (>)
  1373. // Set the self-closing flag of the current tag token. Switch to the data
  1374. // state. Emit the current tag token.
  1375. if (cc === CC_GREATER_THAN) {
  1376. if (input.charCodeAt(tagStart + 1) === CC_SOLIDUS) {
  1377. // An end tag emitted with the self-closing flag set is an
  1378. // end-tag-with-trailing-solidus parse error.
  1379. reportError(
  1380. "end-tag-with-trailing-solidus",
  1381. tagStart,
  1382. pos + 1,
  1383. "warning"
  1384. );
  1385. state = STATE_DATA;
  1386. pos = emitCloseTag(pos + 1);
  1387. } else {
  1388. pos = emitOpenTag(pos + 1, true);
  1389. state = STATE_DATA;
  1390. }
  1391. } else {
  1392. // Anything else
  1393. // This is an unexpected-solidus-in-tag parse error. Reconsume in the before
  1394. // attribute name state.
  1395. reportError("unexpected-solidus-in-tag", pos, pos + 1, "warning");
  1396. state = STATE_BEFORE_ATTRIBUTE_NAME;
  1397. // Reconsume
  1398. }
  1399. break;
  1400. // https://html.spec.whatwg.org/multipage/parsing.html#markup-declaration-open-state
  1401. case STATE_MARKUP_DECLARATION_OPEN:
  1402. // If the next few characters are:
  1403. // Two U+002D HYPHEN-MINUS characters (-)
  1404. // Consume those two characters, create a comment token whose data
  1405. // is the empty string, and switch to the comment start state.
  1406. if (
  1407. cc === CC_HYPHEN_MINUS &&
  1408. input.charCodeAt(pos + 1) === CC_HYPHEN_MINUS
  1409. ) {
  1410. pos += 2;
  1411. commentStart = tagStart;
  1412. commentDataStart = pos;
  1413. commentDataEnd = pos;
  1414. state = STATE_COMMENT_START;
  1415. } else if (
  1416. // ASCII case-insensitive match for the word "DOCTYPE"
  1417. // Consume those characters and switch to the DOCTYPE state.
  1418. (cc === 0x44 || cc === 0x64) /* D or d */ &&
  1419. (input.charCodeAt(pos + 1) | 0x20) === 0x6f /* o */ &&
  1420. (input.charCodeAt(pos + 2) | 0x20) === 0x63 /* c */ &&
  1421. (input.charCodeAt(pos + 3) | 0x20) === 0x74 /* t */ &&
  1422. (input.charCodeAt(pos + 4) | 0x20) === 0x79 /* y */ &&
  1423. (input.charCodeAt(pos + 5) | 0x20) === 0x70 /* p */ &&
  1424. (input.charCodeAt(pos + 6) | 0x20) === 0x65 /* e */
  1425. ) {
  1426. pos += 7;
  1427. commentStart = tagStart;
  1428. doctypeNameStart = -1;
  1429. doctypeNameEnd = -1;
  1430. doctypePublicStart = -1;
  1431. doctypePublicEnd = -1;
  1432. doctypeSystemStart = -1;
  1433. doctypeSystemEnd = -1;
  1434. doctypeForceQuirks = false;
  1435. state = STATE_DOCTYPE;
  1436. } else if (
  1437. // The string "[CDATA[" (the five uppercase letters "CDATA" with a
  1438. // U+005B LEFT SQUARE BRACKET character before and after)
  1439. // Consume those characters and switch to the CDATA section state.
  1440. // Only when there is an adjusted current node and it is not an
  1441. // element in the HTML namespace: everywhere else this is the
  1442. // "anything else" branch below, a bogus comment ending at the first
  1443. // `>`, which leaves the rest (typically `]]>`) as text.
  1444. cc === CC_LEFT_SQUARE_BRACKET &&
  1445. input.charCodeAt(pos + 1) === 0x43 /* C */ &&
  1446. input.charCodeAt(pos + 2) === 0x44 /* D */ &&
  1447. input.charCodeAt(pos + 3) === 0x41 /* A */ &&
  1448. input.charCodeAt(pos + 4) === 0x54 /* T */ &&
  1449. input.charCodeAt(pos + 5) === 0x41 /* A */ &&
  1450. input.charCodeAt(pos + 6) === CC_LEFT_SQUARE_BRACKET &&
  1451. callbacks.isForeign !== undefined &&
  1452. callbacks.isForeign()
  1453. ) {
  1454. pos += 7;
  1455. commentStart = tagStart;
  1456. commentDataStart = pos;
  1457. commentDataEnd = pos;
  1458. state = STATE_CDATA_SECTION;
  1459. } else {
  1460. // Anything else
  1461. // This is an incorrectly-opened-comment parse error. Create a comment token
  1462. // whose data is the empty string. Switch to the bogus comment state (don't
  1463. // consume anything in the current state).
  1464. reportError("incorrectly-opened-comment", tagStart, pos, "warning");
  1465. commentStart = tagStart;
  1466. commentDataStart = pos;
  1467. commentDataEnd = pos;
  1468. state = STATE_BOGUS_COMMENT;
  1469. // Reconsume
  1470. }
  1471. break;
  1472. // https://html.spec.whatwg.org/multipage/parsing.html#comment-start-state
  1473. case STATE_COMMENT_START:
  1474. // Consume the next input character:
  1475. // U+002D HYPHEN-MINUS (-)
  1476. // Switch to the comment start dash state.
  1477. if (cc === CC_HYPHEN_MINUS) {
  1478. state = STATE_COMMENT_START_DASH;
  1479. pos++;
  1480. } else if (cc === CC_GREATER_THAN) {
  1481. // U+003E GREATER-THAN SIGN (>)
  1482. // This is an abrupt-closing-of-empty-comment parse error. Switch to the
  1483. // data state. Emit the current comment token.
  1484. reportError(
  1485. "abrupt-closing-of-empty-comment",
  1486. pos,
  1487. pos + 1,
  1488. "warning"
  1489. );
  1490. let nextPos = pos + 1;
  1491. if (callbacks.comment !== undefined) {
  1492. nextPos = callbacks.comment(
  1493. input,
  1494. commentStart,
  1495. pos + 1,
  1496. commentDataStart,
  1497. commentDataEnd
  1498. );
  1499. }
  1500. state = STATE_DATA;
  1501. textStart = nextPos;
  1502. pos = nextPos;
  1503. } else {
  1504. // Anything else
  1505. // Reconsume in the comment state.
  1506. state = STATE_COMMENT;
  1507. // Reconsume
  1508. }
  1509. break;
  1510. // https://html.spec.whatwg.org/multipage/parsing.html#comment-start-dash-state
  1511. case STATE_COMMENT_START_DASH:
  1512. // Consume the next input character:
  1513. // U+002D HYPHEN-MINUS (-)
  1514. // Switch to the comment end state.
  1515. if (cc === CC_HYPHEN_MINUS) {
  1516. state = STATE_COMMENT_END;
  1517. pos++;
  1518. } else if (cc === CC_GREATER_THAN) {
  1519. // U+003E GREATER-THAN SIGN (>)
  1520. // This is an abrupt-closing-of-empty-comment parse error. Switch to the
  1521. // data state. Emit the current comment token.
  1522. reportError(
  1523. "abrupt-closing-of-empty-comment",
  1524. pos,
  1525. pos + 1,
  1526. "warning"
  1527. );
  1528. let nextPos = pos + 1;
  1529. if (callbacks.comment !== undefined) {
  1530. nextPos = callbacks.comment(
  1531. input,
  1532. commentStart,
  1533. pos + 1,
  1534. commentDataStart,
  1535. commentDataEnd
  1536. );
  1537. }
  1538. state = STATE_DATA;
  1539. textStart = nextPos;
  1540. pos = nextPos;
  1541. } else {
  1542. // Anything else
  1543. // Append a U+002D HYPHEN-MINUS character (-) to the comment token's data.
  1544. // Reconsume in the comment state.
  1545. commentDataEnd = pos;
  1546. state = STATE_COMMENT;
  1547. // Reconsume
  1548. }
  1549. break;
  1550. // https://html.spec.whatwg.org/multipage/parsing.html#comment-state
  1551. case STATE_COMMENT:
  1552. // Consume the next input character:
  1553. // U+003C LESS-THAN SIGN (<)
  1554. // Append a U+003C LESS-THAN SIGN character to the comment token's data. Switch to the comment less-than sign state.
  1555. if (cc === CC_LESS_THAN) {
  1556. state = STATE_COMMENT_LESS_THAN_SIGN;
  1557. pos++;
  1558. commentDataEnd = pos;
  1559. } else if (cc === CC_HYPHEN_MINUS) {
  1560. // Consume the next input character:
  1561. // U+002D HYPHEN-MINUS (-)
  1562. // Switch to the comment end dash state.
  1563. state = STATE_COMMENT_END_DASH;
  1564. pos++;
  1565. } else {
  1566. // U+0000 NULL: unexpected-null-character (append U+FFFD).
  1567. if (cc === CC_NULL) {
  1568. reportError("unexpected-null-character", pos, pos + 1, "warning");
  1569. }
  1570. // Fast-forward over ordinary comment text (same memoized
  1571. // `indexOf` scheme as the data state); stop on `<` / `-` / NUL.
  1572. pos++;
  1573. if (nextLt < pos) {
  1574. nextLt = input.indexOf("<", pos);
  1575. if (nextLt === -1) nextLt = len;
  1576. }
  1577. if (nextHyphen < pos) {
  1578. nextHyphen = input.indexOf("-", pos);
  1579. if (nextHyphen === -1) nextHyphen = len;
  1580. }
  1581. if (nextNul < pos) {
  1582. nextNul = input.indexOf("\0", pos);
  1583. if (nextNul === -1) nextNul = len;
  1584. }
  1585. pos = Math.min(nextLt, nextHyphen, nextNul);
  1586. commentDataEnd = pos;
  1587. }
  1588. break;
  1589. // https://html.spec.whatwg.org/multipage/parsing.html#comment-end-dash-state
  1590. case STATE_COMMENT_END_DASH:
  1591. // Consume the next input character:
  1592. // U+002D HYPHEN-MINUS (-)
  1593. // Switch to the comment end state.
  1594. if (cc === CC_HYPHEN_MINUS) {
  1595. state = STATE_COMMENT_END;
  1596. pos++;
  1597. } else {
  1598. // Anything else
  1599. // Append a U+002D HYPHEN-MINUS character (-) to the comment token's data.
  1600. // Reconsume in the comment state (so e.g. NULL and `<` are
  1601. // handled there).
  1602. commentDataEnd = pos;
  1603. state = STATE_COMMENT;
  1604. // Reconsume
  1605. }
  1606. break;
  1607. // https://html.spec.whatwg.org/multipage/parsing.html#comment-end-state
  1608. case STATE_COMMENT_END:
  1609. // Consume the next input character:
  1610. // U+003E GREATER-THAN SIGN (>)
  1611. // Switch to the data state. Emit the current comment token.
  1612. if (cc === CC_GREATER_THAN) {
  1613. let nextPos = pos + 1;
  1614. if (callbacks.comment !== undefined) {
  1615. nextPos = callbacks.comment(
  1616. input,
  1617. commentStart,
  1618. pos + 1,
  1619. commentDataStart,
  1620. commentDataEnd
  1621. );
  1622. }
  1623. state = STATE_DATA;
  1624. textStart = nextPos;
  1625. pos = nextPos;
  1626. } else if (cc === CC_EXCLAMATION_MARK) {
  1627. // U+0021 EXCLAMATION MARK (!)
  1628. // Switch to the comment end bang state.
  1629. state = STATE_COMMENT_END_BANG;
  1630. pos++;
  1631. } else if (cc === CC_HYPHEN_MINUS) {
  1632. // One of the two pending dashes joins the data; the other stays
  1633. // pending, so the range grows by one rather than to `pos`.
  1634. commentDataEnd++;
  1635. pos++;
  1636. } else {
  1637. // Anything else
  1638. // Append two U+002D HYPHEN-MINUS characters (-) to the comment token's
  1639. // data. Reconsume in the comment state (so NULL and `<` are
  1640. // handled there).
  1641. commentDataEnd = pos;
  1642. state = STATE_COMMENT;
  1643. // Reconsume
  1644. }
  1645. break;
  1646. // https://html.spec.whatwg.org/multipage/parsing.html#comment-end-bang-state
  1647. case STATE_COMMENT_END_BANG:
  1648. // Consume the next input character:
  1649. // U+002D HYPHEN-MINUS (-)
  1650. // Append two U+002D HYPHEN-MINUS characters (-) and a U+0021 EXCLAMATION
  1651. // MARK character (!) to the comment token's data. Switch to the comment end
  1652. // dash state.
  1653. if (cc === CC_HYPHEN_MINUS) {
  1654. commentDataEnd = pos;
  1655. state = STATE_COMMENT_END_DASH;
  1656. pos++;
  1657. } else if (cc === CC_GREATER_THAN) {
  1658. // U+003E GREATER-THAN SIGN (>)
  1659. // This is an incorrectly-closed-comment parse error. Switch to the data
  1660. // state. Emit the current comment token.
  1661. reportError("incorrectly-closed-comment", pos, pos + 1, "warning");
  1662. let nextPos = pos + 1;
  1663. if (callbacks.comment !== undefined) {
  1664. nextPos = callbacks.comment(
  1665. input,
  1666. commentStart,
  1667. pos + 1,
  1668. commentDataStart,
  1669. commentDataEnd
  1670. );
  1671. }
  1672. state = STATE_DATA;
  1673. textStart = nextPos;
  1674. pos = nextPos;
  1675. } else {
  1676. // Anything else
  1677. // Append two U+002D HYPHEN-MINUS characters (-) and a U+0021 EXCLAMATION
  1678. // MARK character (!) to the comment token's data. Reconsume in the comment
  1679. // state (so NULL and `<` are handled there).
  1680. commentDataEnd = pos;
  1681. state = STATE_COMMENT;
  1682. // Reconsume
  1683. }
  1684. break;
  1685. // https://html.spec.whatwg.org/multipage/parsing.html#bogus-comment-state
  1686. case STATE_BOGUS_COMMENT:
  1687. // Consume the next input character:
  1688. // U+003E GREATER-THAN SIGN (>)
  1689. // Switch to the data state. Emit the current comment token.
  1690. if (cc === CC_GREATER_THAN) {
  1691. let nextPos = pos + 1;
  1692. if (callbacks.comment !== undefined) {
  1693. nextPos = callbacks.comment(
  1694. input,
  1695. commentStart,
  1696. pos + 1,
  1697. commentDataStart,
  1698. commentDataEnd
  1699. );
  1700. }
  1701. state = STATE_DATA;
  1702. textStart = nextPos;
  1703. pos = nextPos;
  1704. } else {
  1705. // U+0000 NULL: unexpected-null-character (append U+FFFD).
  1706. if (cc === CC_NULL) {
  1707. reportError("unexpected-null-character", pos, pos + 1, "warning");
  1708. }
  1709. // Fast-forward over ordinary bogus-comment text (memoized `indexOf`
  1710. // scheme); stop on `>` / NUL.
  1711. pos++;
  1712. if (nextGt < pos) {
  1713. nextGt = input.indexOf(">", pos);
  1714. if (nextGt === -1) nextGt = len;
  1715. }
  1716. if (nextNul < pos) {
  1717. nextNul = input.indexOf("\0", pos);
  1718. if (nextNul === -1) nextNul = len;
  1719. }
  1720. pos = Math.min(nextGt, nextNul);
  1721. commentDataEnd = pos;
  1722. }
  1723. break;
  1724. // https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-state
  1725. case STATE_COMMENT_LESS_THAN_SIGN:
  1726. // Consume the next input character:
  1727. // U+0021 EXCLAMATION MARK (!)
  1728. // Append the current input character to the comment token's data. Switch to
  1729. // the comment less-than sign bang state.
  1730. if (cc === CC_EXCLAMATION_MARK) {
  1731. state = STATE_COMMENT_LESS_THAN_SIGN_BANG;
  1732. pos++;
  1733. commentDataEnd = pos;
  1734. } else if (cc === CC_LESS_THAN) {
  1735. // U+003C LESS-THAN SIGN (<)
  1736. // Append the current input character to the comment token's data.
  1737. pos++;
  1738. commentDataEnd = pos;
  1739. } else {
  1740. // Anything else
  1741. // Reconsume in the comment state.
  1742. state = STATE_COMMENT;
  1743. // Reconsume
  1744. }
  1745. break;
  1746. // https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-bang-state
  1747. case STATE_COMMENT_LESS_THAN_SIGN_BANG:
  1748. // Consume the next input character:
  1749. // U+002D HYPHEN-MINUS (-)
  1750. // Switch to the comment less-than sign bang dash state.
  1751. if (cc === CC_HYPHEN_MINUS) {
  1752. state = STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH;
  1753. pos++;
  1754. } else {
  1755. // Anything else
  1756. // Reconsume in the comment state.
  1757. state = STATE_COMMENT;
  1758. // Reconsume
  1759. }
  1760. break;
  1761. // https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-bang-dash-state
  1762. case STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH:
  1763. // Consume the next input character:
  1764. // U+002D HYPHEN-MINUS (-)
  1765. // Switch to the comment less-than sign bang dash dash state.
  1766. if (cc === CC_HYPHEN_MINUS) {
  1767. state = STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH_DASH;
  1768. pos++;
  1769. } else {
  1770. // Anything else
  1771. // Reconsume in the comment end dash state.
  1772. state = STATE_COMMENT_END_DASH;
  1773. // Reconsume
  1774. }
  1775. break;
  1776. // https://html.spec.whatwg.org/multipage/parsing.html#comment-less-than-sign-bang-dash-dash-state
  1777. case STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH_DASH:
  1778. // Consume the next input character:
  1779. // U+003E GREATER-THAN SIGN (>)
  1780. // EOF
  1781. // Reconsume in the comment end state.
  1782. // Anything else
  1783. // This is a nested-comment parse error. Reconsume in the comment end state.
  1784. if (cc !== CC_GREATER_THAN) {
  1785. reportError("nested-comment", pos, pos + 1, "warning");
  1786. }
  1787. state = STATE_COMMENT_END;
  1788. // Reconsume
  1789. break;
  1790. // https://html.spec.whatwg.org/multipage/parsing.html#doctype-state
  1791. case STATE_DOCTYPE:
  1792. // Consume the next input character:
  1793. // U+0009 CHARACTER TABULATION (tab)
  1794. // U+000A LINE FEED (LF)
  1795. // U+000C FORM FEED (FF)
  1796. // U+0020 SPACE
  1797. // Switch to the before DOCTYPE name state.
  1798. if (isSpace(cc)) {
  1799. state = STATE_BEFORE_DOCTYPE_NAME;
  1800. pos++;
  1801. } else if (cc === CC_GREATER_THAN) {
  1802. // U+003E GREATER-THAN SIGN (>)
  1803. // Reconsume in the before DOCTYPE name state.
  1804. state = STATE_BEFORE_DOCTYPE_NAME;
  1805. } else {
  1806. // Anything else
  1807. // This is a missing-whitespace-before-doctype-name parse error. Reconsume
  1808. // in the before DOCTYPE name state.
  1809. reportError(
  1810. "missing-whitespace-before-doctype-name",
  1811. pos,
  1812. pos + 1,
  1813. "warning"
  1814. );
  1815. state = STATE_BEFORE_DOCTYPE_NAME;
  1816. }
  1817. break;
  1818. // https://html.spec.whatwg.org/multipage/parsing.html#before-doctype-name-state
  1819. case STATE_BEFORE_DOCTYPE_NAME:
  1820. // Consume the next input character:
  1821. // U+0009 CHARACTER TABULATION (tab)
  1822. // U+000A LINE FEED (LF)
  1823. // U+000C FORM FEED (FF)
  1824. // U+0020 SPACE
  1825. // Ignore the character.
  1826. if (isSpace(cc)) {
  1827. pos++;
  1828. } else if (cc === CC_NULL) {
  1829. // U+0000 NULL
  1830. // This is an unexpected-null-character parse error. Create a new DOCTYPE
  1831. // token. Set the token's name to a U+FFFD REPLACEMENT CHARACTER character.
  1832. // Switch to the DOCTYPE name state.
  1833. reportError("unexpected-null-character", pos, pos + 1, "warning");
  1834. state = STATE_DOCTYPE_NAME;
  1835. doctypeNameStart = pos;
  1836. pos++;
  1837. doctypeNameEnd = pos;
  1838. } else if (cc === CC_GREATER_THAN) {
  1839. // U+003E GREATER-THAN SIGN (>)
  1840. // This is a missing-doctype-name parse error. Create a new DOCTYPE token.
  1841. // Set its force-quirks flag to on. Switch to the data state. Emit the
  1842. // current token.
  1843. reportError("missing-doctype-name", pos, pos + 1, "warning");
  1844. doctypeForceQuirks = true;
  1845. let nextPos = pos + 1;
  1846. if (callbacks.doctype !== undefined) {
  1847. nextPos = callbacks.doctype(
  1848. input,
  1849. commentStart,
  1850. pos + 1,
  1851. doctypeNameStart,
  1852. doctypeNameEnd,
  1853. doctypePublicStart,
  1854. doctypePublicEnd,
  1855. doctypeSystemStart,
  1856. doctypeSystemEnd,
  1857. doctypeForceQuirks
  1858. );
  1859. }
  1860. state = STATE_DATA;
  1861. textStart = nextPos;
  1862. pos = nextPos;
  1863. } else {
  1864. // ASCII upper alpha
  1865. // Create a new DOCTYPE token. Set the token's name to the lowercase version
  1866. // of the current input character (add 0x0020 to the character's code
  1867. // point). Switch to the DOCTYPE name state.
  1868. // Anything else
  1869. // Create a new DOCTYPE token. Set the token's name to the current input
  1870. // character. Switch to the DOCTYPE name state.
  1871. state = STATE_DOCTYPE_NAME;
  1872. doctypeNameStart = pos;
  1873. pos++;
  1874. doctypeNameEnd = pos;
  1875. }
  1876. break;
  1877. // https://html.spec.whatwg.org/multipage/parsing.html#doctype-name-state
  1878. case STATE_DOCTYPE_NAME:
  1879. // Consume the next input character:
  1880. // U+0009 CHARACTER TABULATION (tab)
  1881. // U+000A LINE FEED (LF)
  1882. // U+000C FORM FEED (FF)
  1883. // U+0020 SPACE
  1884. // Switch to the after DOCTYPE name state.
  1885. if (isSpace(cc)) {
  1886. state = STATE_AFTER_DOCTYPE_NAME;
  1887. pos++;
  1888. } else if (cc === CC_GREATER_THAN) {
  1889. // U+003E GREATER-THAN SIGN (>)
  1890. // Switch to the data state. Emit the current DOCTYPE token.
  1891. let nextPos = pos + 1;
  1892. if (callbacks.doctype !== undefined) {
  1893. nextPos = callbacks.doctype(
  1894. input,
  1895. commentStart,
  1896. pos + 1,
  1897. doctypeNameStart,
  1898. doctypeNameEnd,
  1899. doctypePublicStart,
  1900. doctypePublicEnd,
  1901. doctypeSystemStart,
  1902. doctypeSystemEnd,
  1903. doctypeForceQuirks
  1904. );
  1905. }
  1906. state = STATE_DATA;
  1907. textStart = nextPos;
  1908. pos = nextPos;
  1909. } else if (cc === CC_NULL) {
  1910. // U+0000 NULL
  1911. // This is an unexpected-null-character parse error. Append a U+FFFD
  1912. // REPLACEMENT CHARACTER character to the current DOCTYPE token's name.
  1913. reportError("unexpected-null-character", pos, pos + 1, "warning");
  1914. pos++;
  1915. doctypeNameEnd = pos;
  1916. } else {
  1917. // ASCII upper alpha
  1918. // Append the lowercase version of the current input character (add 0x0020
  1919. // to the character's code point) to the current DOCTYPE token's name.
  1920. // Anything else
  1921. // Append the current input character to the current DOCTYPE token's name.
  1922. pos++;
  1923. doctypeNameEnd = pos;
  1924. }
  1925. break;
  1926. // https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-name-state
  1927. case STATE_AFTER_DOCTYPE_NAME:
  1928. // Consume the next input character:
  1929. if (isSpace(cc)) {
  1930. // U+0009 CHARACTER TABULATION (tab)
  1931. // U+000A LINE FEED (LF)
  1932. // U+000C FORM FEED (FF)
  1933. // U+0020 SPACE
  1934. // Ignore the character.
  1935. pos++;
  1936. } else if (cc === CC_GREATER_THAN) {
  1937. // U+003E GREATER-THAN SIGN (>)
  1938. // Switch to the data state. Emit the current DOCTYPE token.
  1939. let nextPos = pos + 1;
  1940. if (callbacks.doctype !== undefined) {
  1941. nextPos = callbacks.doctype(
  1942. input,
  1943. commentStart,
  1944. pos + 1,
  1945. doctypeNameStart,
  1946. doctypeNameEnd,
  1947. doctypePublicStart,
  1948. doctypePublicEnd,
  1949. doctypeSystemStart,
  1950. doctypeSystemEnd,
  1951. doctypeForceQuirks
  1952. );
  1953. }
  1954. state = STATE_DATA;
  1955. textStart = nextPos;
  1956. pos = nextPos;
  1957. } else if (
  1958. pos + 5 < len &&
  1959. (cc === 0x50 || cc === 0x70) /* P or p */ &&
  1960. (input.charCodeAt(pos + 1) | 0x20) === 0x75 /* u */ &&
  1961. (input.charCodeAt(pos + 2) | 0x20) === 0x62 /* b */ &&
  1962. (input.charCodeAt(pos + 3) | 0x20) === 0x6c /* l */ &&
  1963. (input.charCodeAt(pos + 4) | 0x20) === 0x69 /* i */ &&
  1964. (input.charCodeAt(pos + 5) | 0x20) === 0x63 /* c */
  1965. ) {
  1966. // ASCII case-insensitive match for the word "PUBLIC"
  1967. pos += 6;
  1968. state = STATE_AFTER_DOCTYPE_PUBLIC_KEYWORD;
  1969. } else if (
  1970. pos + 5 < len &&
  1971. (cc === 0x53 || cc === 0x73) /* S or s */ &&
  1972. (input.charCodeAt(pos + 1) | 0x20) === 0x79 /* y */ &&
  1973. (input.charCodeAt(pos + 2) | 0x20) === 0x73 /* s */ &&
  1974. (input.charCodeAt(pos + 3) | 0x20) === 0x74 /* t */ &&
  1975. (input.charCodeAt(pos + 4) | 0x20) === 0x65 /* e */ &&
  1976. (input.charCodeAt(pos + 5) | 0x20) === 0x6d /* m */
  1977. ) {
  1978. // ASCII case-insensitive match for the word "SYSTEM"
  1979. pos += 6;
  1980. state = STATE_AFTER_DOCTYPE_SYSTEM_KEYWORD;
  1981. } else {
  1982. // Anything else
  1983. // This is an invalid-character-sequence-after-doctype-name parse error. Set
  1984. // the current DOCTYPE token's force-quirks flag to on. Reconsume in the
  1985. // bogus DOCTYPE state.
  1986. reportError(
  1987. "invalid-character-sequence-after-doctype-name",
  1988. pos,
  1989. pos + 1,
  1990. "warning"
  1991. );
  1992. doctypeForceQuirks = true;
  1993. state = STATE_BOGUS_DOCTYPE;
  1994. }
  1995. break;
  1996. // https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-public-keyword-state
  1997. case STATE_AFTER_DOCTYPE_PUBLIC_KEYWORD:
  1998. // Consume the next input character:
  1999. if (isSpace(cc)) {
  2000. // U+0009 CHARACTER TABULATION (tab)
  2001. // U+000A LINE FEED (LF)
  2002. // U+000C FORM FEED (FF)
  2003. // U+0020 SPACE
  2004. // Switch to the before DOCTYPE public identifier state.
  2005. state = STATE_BEFORE_DOCTYPE_PUBLIC_IDENTIFIER;
  2006. pos++;
  2007. } else if (cc === CC_QUOTATION_MARK) {
  2008. // U+0022 QUOTATION MARK (")
  2009. // This is a missing-whitespace-after-doctype-public-keyword parse error.
  2010. // Set the current DOCTYPE token's public identifier to the empty string
  2011. // (not missing), then switch to the DOCTYPE public identifier
  2012. // (double-quoted) state.
  2013. reportError(
  2014. "missing-whitespace-after-doctype-public-keyword",
  2015. pos,
  2016. pos + 1,
  2017. "warning"
  2018. );
  2019. state = STATE_DOCTYPE_PUBLIC_IDENTIFIER_DOUBLE_QUOTED;
  2020. pos++;
  2021. doctypePublicStart = pos;
  2022. doctypePublicEnd = pos;
  2023. } else if (cc === CC_APOSTROPHE) {
  2024. // U+0027 APOSTROPHE (')
  2025. // This is a missing-whitespace-after-doctype-public-keyword parse error.
  2026. // Set the current DOCTYPE token's public identifier to the empty string
  2027. // (not missing), then switch to the DOCTYPE public identifier
  2028. // (single-quoted) state.
  2029. reportError(
  2030. "missing-whitespace-after-doctype-public-keyword",
  2031. pos,
  2032. pos + 1,
  2033. "warning"
  2034. );
  2035. state = STATE_DOCTYPE_PUBLIC_IDENTIFIER_SINGLE_QUOTED;
  2036. pos++;
  2037. doctypePublicStart = pos;
  2038. doctypePublicEnd = pos;
  2039. } else if (cc === CC_GREATER_THAN) {
  2040. // U+003E GREATER-THAN SIGN (>)
  2041. // This is a missing-doctype-public-identifier parse error. Set the current
  2042. // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
  2043. // the current DOCTYPE token.
  2044. reportError(
  2045. "missing-doctype-public-identifier",
  2046. pos,
  2047. pos + 1,
  2048. "warning"
  2049. );
  2050. doctypeForceQuirks = true;
  2051. let nextPos = pos + 1;
  2052. if (callbacks.doctype !== undefined) {
  2053. nextPos = callbacks.doctype(
  2054. input,
  2055. commentStart,
  2056. pos + 1,
  2057. doctypeNameStart,
  2058. doctypeNameEnd,
  2059. doctypePublicStart,
  2060. doctypePublicEnd,
  2061. doctypeSystemStart,
  2062. doctypeSystemEnd,
  2063. doctypeForceQuirks
  2064. );
  2065. }
  2066. state = STATE_DATA;
  2067. textStart = nextPos;
  2068. pos = nextPos;
  2069. } else {
  2070. // Anything else
  2071. // This is a missing-quote-before-doctype-public-identifier parse error. Set
  2072. // the current DOCTYPE token's force-quirks flag to on. Reconsume in the
  2073. // bogus DOCTYPE state.
  2074. reportError(
  2075. "missing-quote-before-doctype-public-identifier",
  2076. pos,
  2077. pos + 1,
  2078. "warning"
  2079. );
  2080. doctypeForceQuirks = true;
  2081. state = STATE_BOGUS_DOCTYPE;
  2082. }
  2083. break;
  2084. // https://html.spec.whatwg.org/multipage/parsing.html#before-doctype-public-identifier-state
  2085. case STATE_BEFORE_DOCTYPE_PUBLIC_IDENTIFIER:
  2086. // Consume the next input character:
  2087. if (isSpace(cc)) {
  2088. // U+0009 CHARACTER TABULATION (tab)
  2089. // U+000A LINE FEED (LF)
  2090. // U+000C FORM FEED (FF)
  2091. // U+0020 SPACE
  2092. // Ignore the character.
  2093. pos++;
  2094. } else if (cc === CC_QUOTATION_MARK) {
  2095. // U+0022 QUOTATION MARK (")
  2096. // Set the current DOCTYPE token's public identifier to the empty string
  2097. // (not missing), then switch to the DOCTYPE public identifier
  2098. // (double-quoted) state.
  2099. state = STATE_DOCTYPE_PUBLIC_IDENTIFIER_DOUBLE_QUOTED;
  2100. pos++;
  2101. doctypePublicStart = pos;
  2102. doctypePublicEnd = pos;
  2103. } else if (cc === CC_APOSTROPHE) {
  2104. // U+0027 APOSTROPHE (')
  2105. // Set the current DOCTYPE token's public identifier to the empty string
  2106. // (not missing), then switch to the DOCTYPE public identifier
  2107. // (single-quoted) state.
  2108. state = STATE_DOCTYPE_PUBLIC_IDENTIFIER_SINGLE_QUOTED;
  2109. pos++;
  2110. doctypePublicStart = pos;
  2111. doctypePublicEnd = pos;
  2112. } else if (cc === CC_GREATER_THAN) {
  2113. // U+003E GREATER-THAN SIGN (>)
  2114. // This is a missing-doctype-public-identifier parse error. Set the current
  2115. // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
  2116. // the current DOCTYPE token.
  2117. reportError(
  2118. "missing-doctype-public-identifier",
  2119. pos,
  2120. pos + 1,
  2121. "warning"
  2122. );
  2123. doctypeForceQuirks = true;
  2124. let nextPos = pos + 1;
  2125. if (callbacks.doctype !== undefined) {
  2126. nextPos = callbacks.doctype(
  2127. input,
  2128. commentStart,
  2129. pos + 1,
  2130. doctypeNameStart,
  2131. doctypeNameEnd,
  2132. doctypePublicStart,
  2133. doctypePublicEnd,
  2134. doctypeSystemStart,
  2135. doctypeSystemEnd,
  2136. doctypeForceQuirks
  2137. );
  2138. }
  2139. state = STATE_DATA;
  2140. textStart = nextPos;
  2141. pos = nextPos;
  2142. } else {
  2143. // Anything else
  2144. // This is a missing-quote-before-doctype-public-identifier parse error. Set
  2145. // the current DOCTYPE token's force-quirks flag to on. Reconsume in the
  2146. // bogus DOCTYPE state.
  2147. reportError(
  2148. "missing-quote-before-doctype-public-identifier",
  2149. pos,
  2150. pos + 1,
  2151. "warning"
  2152. );
  2153. doctypeForceQuirks = true;
  2154. state = STATE_BOGUS_DOCTYPE;
  2155. }
  2156. break;
  2157. // https://html.spec.whatwg.org/multipage/parsing.html#doctype-public-identifier-(double-quoted)-state
  2158. case STATE_DOCTYPE_PUBLIC_IDENTIFIER_DOUBLE_QUOTED:
  2159. // Consume the next input character:
  2160. if (cc === CC_QUOTATION_MARK) {
  2161. // U+0022 QUOTATION MARK (")
  2162. // Switch to the after DOCTYPE public identifier state.
  2163. state = STATE_AFTER_DOCTYPE_PUBLIC_IDENTIFIER;
  2164. pos++;
  2165. } else if (cc === CC_NULL) {
  2166. // U+0000 NULL
  2167. // This is an unexpected-null-character parse error. Append a U+FFFD
  2168. // REPLACEMENT CHARACTER character to the current DOCTYPE token's public
  2169. // identifier.
  2170. reportError("unexpected-null-character", pos, pos + 1, "warning");
  2171. pos++;
  2172. doctypePublicEnd = pos;
  2173. } else if (cc === CC_GREATER_THAN) {
  2174. // U+003E GREATER-THAN SIGN (>)
  2175. // This is an abrupt-doctype-public-identifier parse error. Set the current
  2176. // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
  2177. // the current DOCTYPE token.
  2178. reportError(
  2179. "abrupt-doctype-public-identifier",
  2180. pos,
  2181. pos + 1,
  2182. "warning"
  2183. );
  2184. doctypeForceQuirks = true;
  2185. let nextPos = pos + 1;
  2186. if (callbacks.doctype !== undefined) {
  2187. nextPos = callbacks.doctype(
  2188. input,
  2189. commentStart,
  2190. pos + 1,
  2191. doctypeNameStart,
  2192. doctypeNameEnd,
  2193. doctypePublicStart,
  2194. doctypePublicEnd,
  2195. doctypeSystemStart,
  2196. doctypeSystemEnd,
  2197. doctypeForceQuirks
  2198. );
  2199. }
  2200. state = STATE_DATA;
  2201. textStart = nextPos;
  2202. pos = nextPos;
  2203. } else {
  2204. // Anything else
  2205. // Append the current input character to the current DOCTYPE token's public
  2206. // identifier.
  2207. pos++;
  2208. doctypePublicEnd = pos;
  2209. }
  2210. break;
  2211. // https://html.spec.whatwg.org/multipage/parsing.html#doctype-public-identifier-(single-quoted)-state
  2212. case STATE_DOCTYPE_PUBLIC_IDENTIFIER_SINGLE_QUOTED:
  2213. // Consume the next input character:
  2214. if (cc === CC_APOSTROPHE) {
  2215. // U+0027 APOSTROPHE (')
  2216. // Switch to the after DOCTYPE public identifier state.
  2217. state = STATE_AFTER_DOCTYPE_PUBLIC_IDENTIFIER;
  2218. pos++;
  2219. } else if (cc === CC_NULL) {
  2220. // U+0000 NULL
  2221. // This is an unexpected-null-character parse error. Append a U+FFFD
  2222. // REPLACEMENT CHARACTER character to the current DOCTYPE token's public
  2223. // identifier.
  2224. reportError("unexpected-null-character", pos, pos + 1, "warning");
  2225. pos++;
  2226. doctypePublicEnd = pos;
  2227. } else if (cc === CC_GREATER_THAN) {
  2228. // U+003E GREATER-THAN SIGN (>)
  2229. // This is an abrupt-doctype-public-identifier parse error. Set the current
  2230. // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
  2231. // the current DOCTYPE token.
  2232. reportError(
  2233. "abrupt-doctype-public-identifier",
  2234. pos,
  2235. pos + 1,
  2236. "warning"
  2237. );
  2238. doctypeForceQuirks = true;
  2239. let nextPos = pos + 1;
  2240. if (callbacks.doctype !== undefined) {
  2241. nextPos = callbacks.doctype(
  2242. input,
  2243. commentStart,
  2244. pos + 1,
  2245. doctypeNameStart,
  2246. doctypeNameEnd,
  2247. doctypePublicStart,
  2248. doctypePublicEnd,
  2249. doctypeSystemStart,
  2250. doctypeSystemEnd,
  2251. doctypeForceQuirks
  2252. );
  2253. }
  2254. state = STATE_DATA;
  2255. textStart = nextPos;
  2256. pos = nextPos;
  2257. } else {
  2258. // Anything else
  2259. // Append the current input character to the current DOCTYPE token's public
  2260. // identifier.
  2261. pos++;
  2262. doctypePublicEnd = pos;
  2263. }
  2264. break;
  2265. // https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-public-identifier-state
  2266. case STATE_AFTER_DOCTYPE_PUBLIC_IDENTIFIER:
  2267. // Consume the next input character:
  2268. if (isSpace(cc)) {
  2269. // U+0009 CHARACTER TABULATION (tab)
  2270. // U+000A LINE FEED (LF)
  2271. // U+000C FORM FEED (FF)
  2272. // U+0020 SPACE
  2273. // Switch to the between DOCTYPE public and system identifiers state.
  2274. state = STATE_BETWEEN_DOCTYPE_PUBLIC_AND_SYSTEM_IDENTIFIERS;
  2275. pos++;
  2276. } else if (cc === CC_GREATER_THAN) {
  2277. // U+003E GREATER-THAN SIGN (>)
  2278. // Switch to the data state. Emit the current DOCTYPE token.
  2279. let nextPos = pos + 1;
  2280. if (callbacks.doctype !== undefined) {
  2281. nextPos = callbacks.doctype(
  2282. input,
  2283. commentStart,
  2284. pos + 1,
  2285. doctypeNameStart,
  2286. doctypeNameEnd,
  2287. doctypePublicStart,
  2288. doctypePublicEnd,
  2289. doctypeSystemStart,
  2290. doctypeSystemEnd,
  2291. doctypeForceQuirks
  2292. );
  2293. }
  2294. state = STATE_DATA;
  2295. textStart = nextPos;
  2296. pos = nextPos;
  2297. } else if (cc === CC_QUOTATION_MARK) {
  2298. // U+0022 QUOTATION MARK (")
  2299. // This is a missing-whitespace-between-doctype-public-and-system-identifiers
  2300. // parse error. Set the current DOCTYPE token's system
  2301. // identifier to the empty string (not missing), then switch
  2302. // to the DOCTYPE system identifier (double-quoted) state.
  2303. reportError(
  2304. "missing-whitespace-between-doctype-public-and-system-identifiers",
  2305. pos,
  2306. pos + 1,
  2307. "warning"
  2308. );
  2309. state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED;
  2310. pos++;
  2311. doctypeSystemStart = pos;
  2312. doctypeSystemEnd = pos;
  2313. } else if (cc === CC_APOSTROPHE) {
  2314. // U+0027 APOSTROPHE (')
  2315. // This is a missing-whitespace-between-doctype-public-and-system-identifiers
  2316. // parse error. Set the current DOCTYPE token's system
  2317. // identifier to the empty string (not missing), then switch
  2318. // to the DOCTYPE system identifier (single-quoted) state.
  2319. reportError(
  2320. "missing-whitespace-between-doctype-public-and-system-identifiers",
  2321. pos,
  2322. pos + 1,
  2323. "warning"
  2324. );
  2325. state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED;
  2326. pos++;
  2327. doctypeSystemStart = pos;
  2328. doctypeSystemEnd = pos;
  2329. } else {
  2330. // Anything else
  2331. // This is a missing-quote-before-doctype-system-identifier parse error. Set
  2332. // the current DOCTYPE token's force-quirks flag to on. Reconsume in the
  2333. // bogus DOCTYPE state.
  2334. reportError(
  2335. "missing-quote-before-doctype-system-identifier",
  2336. pos,
  2337. pos + 1,
  2338. "warning"
  2339. );
  2340. doctypeForceQuirks = true;
  2341. state = STATE_BOGUS_DOCTYPE;
  2342. }
  2343. break;
  2344. // https://html.spec.whatwg.org/multipage/parsing.html#between-doctype-public-and-system-identifiers-state
  2345. case STATE_BETWEEN_DOCTYPE_PUBLIC_AND_SYSTEM_IDENTIFIERS:
  2346. // Consume the next input character:
  2347. if (isSpace(cc)) {
  2348. // U+0009 CHARACTER TABULATION (tab)
  2349. // U+000A LINE FEED (LF)
  2350. // U+000C FORM FEED (FF)
  2351. // U+0020 SPACE
  2352. // Ignore the character.
  2353. pos++;
  2354. } else if (cc === CC_GREATER_THAN) {
  2355. // U+003E GREATER-THAN SIGN (>)
  2356. // Switch to the data state. Emit the current DOCTYPE token.
  2357. let nextPos = pos + 1;
  2358. if (callbacks.doctype !== undefined) {
  2359. nextPos = callbacks.doctype(
  2360. input,
  2361. commentStart,
  2362. pos + 1,
  2363. doctypeNameStart,
  2364. doctypeNameEnd,
  2365. doctypePublicStart,
  2366. doctypePublicEnd,
  2367. doctypeSystemStart,
  2368. doctypeSystemEnd,
  2369. doctypeForceQuirks
  2370. );
  2371. }
  2372. state = STATE_DATA;
  2373. textStart = nextPos;
  2374. pos = nextPos;
  2375. } else if (cc === CC_QUOTATION_MARK) {
  2376. // U+0022 QUOTATION MARK (")
  2377. // Set the current DOCTYPE token's system identifier to the empty string
  2378. // (not missing), then switch to the DOCTYPE system identifier
  2379. // (double-quoted) state.
  2380. state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED;
  2381. pos++;
  2382. doctypeSystemStart = pos;
  2383. doctypeSystemEnd = pos;
  2384. } else if (cc === CC_APOSTROPHE) {
  2385. // U+0027 APOSTROPHE (')
  2386. // Set the current DOCTYPE token's system identifier to the empty string
  2387. // (not missing), then switch to the DOCTYPE system identifier
  2388. // (single-quoted) state.
  2389. state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED;
  2390. pos++;
  2391. doctypeSystemStart = pos;
  2392. doctypeSystemEnd = pos;
  2393. } else {
  2394. // Anything else
  2395. // This is a missing-quote-before-doctype-system-identifier parse error. Set
  2396. // the current DOCTYPE token's force-quirks flag to on. Reconsume in the
  2397. // bogus DOCTYPE state.
  2398. reportError(
  2399. "missing-quote-before-doctype-system-identifier",
  2400. pos,
  2401. pos + 1,
  2402. "warning"
  2403. );
  2404. doctypeForceQuirks = true;
  2405. state = STATE_BOGUS_DOCTYPE;
  2406. }
  2407. break;
  2408. // https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-system-keyword-state
  2409. case STATE_AFTER_DOCTYPE_SYSTEM_KEYWORD:
  2410. // Consume the next input character:
  2411. if (isSpace(cc)) {
  2412. // U+0009 CHARACTER TABULATION (tab)
  2413. // U+000A LINE FEED (LF)
  2414. // U+000C FORM FEED (FF)
  2415. // U+0020 SPACE
  2416. // Switch to the before DOCTYPE system identifier state.
  2417. state = STATE_BEFORE_DOCTYPE_SYSTEM_IDENTIFIER;
  2418. pos++;
  2419. } else if (cc === CC_QUOTATION_MARK) {
  2420. // U+0022 QUOTATION MARK (")
  2421. // This is a missing-whitespace-after-doctype-system-keyword parse error.
  2422. // Set the current DOCTYPE token's system identifier to the empty string
  2423. // (not missing), then switch to the DOCTYPE system identifier
  2424. // (double-quoted) state.
  2425. reportError(
  2426. "missing-whitespace-after-doctype-system-keyword",
  2427. pos,
  2428. pos + 1,
  2429. "warning"
  2430. );
  2431. state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED;
  2432. pos++;
  2433. doctypeSystemStart = pos;
  2434. doctypeSystemEnd = pos;
  2435. } else if (cc === CC_APOSTROPHE) {
  2436. // U+0027 APOSTROPHE (')
  2437. // This is a missing-whitespace-after-doctype-system-keyword parse error.
  2438. // Set the current DOCTYPE token's system identifier to the empty string
  2439. // (not missing), then switch to the DOCTYPE system identifier
  2440. // (single-quoted) state.
  2441. reportError(
  2442. "missing-whitespace-after-doctype-system-keyword",
  2443. pos,
  2444. pos + 1,
  2445. "warning"
  2446. );
  2447. state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED;
  2448. pos++;
  2449. doctypeSystemStart = pos;
  2450. doctypeSystemEnd = pos;
  2451. } else if (cc === CC_GREATER_THAN) {
  2452. // U+003E GREATER-THAN SIGN (>)
  2453. // This is a missing-doctype-system-identifier parse error. Set the current
  2454. // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
  2455. // the current DOCTYPE token.
  2456. reportError(
  2457. "missing-doctype-system-identifier",
  2458. pos,
  2459. pos + 1,
  2460. "warning"
  2461. );
  2462. doctypeForceQuirks = true;
  2463. let nextPos = pos + 1;
  2464. if (callbacks.doctype !== undefined) {
  2465. nextPos = callbacks.doctype(
  2466. input,
  2467. commentStart,
  2468. pos + 1,
  2469. doctypeNameStart,
  2470. doctypeNameEnd,
  2471. doctypePublicStart,
  2472. doctypePublicEnd,
  2473. doctypeSystemStart,
  2474. doctypeSystemEnd,
  2475. doctypeForceQuirks
  2476. );
  2477. }
  2478. state = STATE_DATA;
  2479. textStart = nextPos;
  2480. pos = nextPos;
  2481. } else {
  2482. // Anything else
  2483. // This is a missing-quote-before-doctype-system-identifier parse error. Set
  2484. // the current DOCTYPE token's force-quirks flag to on. Reconsume in the
  2485. // bogus DOCTYPE state.
  2486. reportError(
  2487. "missing-quote-before-doctype-system-identifier",
  2488. pos,
  2489. pos + 1,
  2490. "warning"
  2491. );
  2492. doctypeForceQuirks = true;
  2493. state = STATE_BOGUS_DOCTYPE;
  2494. }
  2495. break;
  2496. // https://html.spec.whatwg.org/multipage/parsing.html#before-doctype-system-identifier-state
  2497. case STATE_BEFORE_DOCTYPE_SYSTEM_IDENTIFIER:
  2498. // Consume the next input character:
  2499. if (isSpace(cc)) {
  2500. // U+0009 CHARACTER TABULATION (tab)
  2501. // U+000A LINE FEED (LF)
  2502. // U+000C FORM FEED (FF)
  2503. // U+0020 SPACE
  2504. // Ignore the character.
  2505. pos++;
  2506. } else if (cc === CC_QUOTATION_MARK) {
  2507. // U+0022 QUOTATION MARK (")
  2508. // Set the current DOCTYPE token's system identifier to the empty string
  2509. // (not missing), then switch to the DOCTYPE system identifier
  2510. // (double-quoted) state.
  2511. state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED;
  2512. pos++;
  2513. doctypeSystemStart = pos;
  2514. doctypeSystemEnd = pos;
  2515. } else if (cc === CC_APOSTROPHE) {
  2516. // U+0027 APOSTROPHE (')
  2517. // Set the current DOCTYPE token's system identifier to the empty string
  2518. // (not missing), then switch to the DOCTYPE system identifier
  2519. // (single-quoted) state.
  2520. state = STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED;
  2521. pos++;
  2522. doctypeSystemStart = pos;
  2523. doctypeSystemEnd = pos;
  2524. } else if (cc === CC_GREATER_THAN) {
  2525. // U+003E GREATER-THAN SIGN (>)
  2526. // This is a missing-doctype-system-identifier parse error. Set the current
  2527. // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
  2528. // the current DOCTYPE token.
  2529. reportError(
  2530. "missing-doctype-system-identifier",
  2531. pos,
  2532. pos + 1,
  2533. "warning"
  2534. );
  2535. doctypeForceQuirks = true;
  2536. let nextPos = pos + 1;
  2537. if (callbacks.doctype !== undefined) {
  2538. nextPos = callbacks.doctype(
  2539. input,
  2540. commentStart,
  2541. pos + 1,
  2542. doctypeNameStart,
  2543. doctypeNameEnd,
  2544. doctypePublicStart,
  2545. doctypePublicEnd,
  2546. doctypeSystemStart,
  2547. doctypeSystemEnd,
  2548. doctypeForceQuirks
  2549. );
  2550. }
  2551. state = STATE_DATA;
  2552. textStart = nextPos;
  2553. pos = nextPos;
  2554. } else {
  2555. // Anything else
  2556. // This is a missing-quote-before-doctype-system-identifier parse error. Set
  2557. // the current DOCTYPE token's force-quirks flag to on. Reconsume in the
  2558. // bogus DOCTYPE state.
  2559. reportError(
  2560. "missing-quote-before-doctype-system-identifier",
  2561. pos,
  2562. pos + 1,
  2563. "warning"
  2564. );
  2565. doctypeForceQuirks = true;
  2566. state = STATE_BOGUS_DOCTYPE;
  2567. }
  2568. break;
  2569. // https://html.spec.whatwg.org/multipage/parsing.html#doctype-system-identifier-(double-quoted)-state
  2570. case STATE_DOCTYPE_SYSTEM_IDENTIFIER_DOUBLE_QUOTED:
  2571. // Consume the next input character:
  2572. if (cc === CC_QUOTATION_MARK) {
  2573. // U+0022 QUOTATION MARK (")
  2574. // Switch to the after DOCTYPE system identifier state.
  2575. state = STATE_AFTER_DOCTYPE_SYSTEM_IDENTIFIER;
  2576. pos++;
  2577. } else if (cc === CC_NULL) {
  2578. // U+0000 NULL
  2579. // This is an unexpected-null-character parse error. Append a U+FFFD
  2580. // REPLACEMENT CHARACTER character to the current DOCTYPE token's system
  2581. // identifier.
  2582. reportError("unexpected-null-character", pos, pos + 1, "warning");
  2583. pos++;
  2584. doctypeSystemEnd = pos;
  2585. } else if (cc === CC_GREATER_THAN) {
  2586. // U+003E GREATER-THAN SIGN (>)
  2587. // This is an abrupt-doctype-system-identifier parse error. Set the current
  2588. // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
  2589. // the current DOCTYPE token.
  2590. reportError(
  2591. "abrupt-doctype-system-identifier",
  2592. pos,
  2593. pos + 1,
  2594. "warning"
  2595. );
  2596. doctypeForceQuirks = true;
  2597. let nextPos = pos + 1;
  2598. if (callbacks.doctype !== undefined) {
  2599. nextPos = callbacks.doctype(
  2600. input,
  2601. commentStart,
  2602. pos + 1,
  2603. doctypeNameStart,
  2604. doctypeNameEnd,
  2605. doctypePublicStart,
  2606. doctypePublicEnd,
  2607. doctypeSystemStart,
  2608. doctypeSystemEnd,
  2609. doctypeForceQuirks
  2610. );
  2611. }
  2612. state = STATE_DATA;
  2613. textStart = nextPos;
  2614. pos = nextPos;
  2615. } else {
  2616. // Anything else
  2617. // Append the current input character to the current DOCTYPE token's system
  2618. // identifier.
  2619. pos++;
  2620. doctypeSystemEnd = pos;
  2621. }
  2622. break;
  2623. // https://html.spec.whatwg.org/multipage/parsing.html#doctype-system-identifier-(single-quoted)-state
  2624. case STATE_DOCTYPE_SYSTEM_IDENTIFIER_SINGLE_QUOTED:
  2625. // Consume the next input character:
  2626. if (cc === CC_APOSTROPHE) {
  2627. // U+0027 APOSTROPHE (')
  2628. // Switch to the after DOCTYPE system identifier state.
  2629. state = STATE_AFTER_DOCTYPE_SYSTEM_IDENTIFIER;
  2630. pos++;
  2631. } else if (cc === CC_NULL) {
  2632. // U+0000 NULL
  2633. // This is an unexpected-null-character parse error. Append a U+FFFD
  2634. // REPLACEMENT CHARACTER character to the current DOCTYPE token's system
  2635. // identifier.
  2636. reportError("unexpected-null-character", pos, pos + 1, "warning");
  2637. pos++;
  2638. doctypeSystemEnd = pos;
  2639. } else if (cc === CC_GREATER_THAN) {
  2640. // U+003E GREATER-THAN SIGN (>)
  2641. // This is an abrupt-doctype-system-identifier parse error. Set the current
  2642. // DOCTYPE token's force-quirks flag to on. Switch to the data state. Emit
  2643. // the current DOCTYPE token.
  2644. reportError(
  2645. "abrupt-doctype-system-identifier",
  2646. pos,
  2647. pos + 1,
  2648. "warning"
  2649. );
  2650. doctypeForceQuirks = true;
  2651. let nextPos = pos + 1;
  2652. if (callbacks.doctype !== undefined) {
  2653. nextPos = callbacks.doctype(
  2654. input,
  2655. commentStart,
  2656. pos + 1,
  2657. doctypeNameStart,
  2658. doctypeNameEnd,
  2659. doctypePublicStart,
  2660. doctypePublicEnd,
  2661. doctypeSystemStart,
  2662. doctypeSystemEnd,
  2663. doctypeForceQuirks
  2664. );
  2665. }
  2666. state = STATE_DATA;
  2667. textStart = nextPos;
  2668. pos = nextPos;
  2669. } else {
  2670. // Anything else
  2671. // Append the current input character to the current DOCTYPE token's system
  2672. // identifier.
  2673. pos++;
  2674. doctypeSystemEnd = pos;
  2675. }
  2676. break;
  2677. // https://html.spec.whatwg.org/multipage/parsing.html#after-doctype-system-identifier-state
  2678. case STATE_AFTER_DOCTYPE_SYSTEM_IDENTIFIER:
  2679. // Consume the next input character:
  2680. if (isSpace(cc)) {
  2681. // U+0009 CHARACTER TABULATION (tab)
  2682. // U+000A LINE FEED (LF)
  2683. // U+000C FORM FEED (FF)
  2684. // U+0020 SPACE
  2685. // Ignore the character.
  2686. pos++;
  2687. } else if (cc === CC_GREATER_THAN) {
  2688. // U+003E GREATER-THAN SIGN (>)
  2689. // Switch to the data state. Emit the current DOCTYPE token.
  2690. let nextPos = pos + 1;
  2691. if (callbacks.doctype !== undefined) {
  2692. nextPos = callbacks.doctype(
  2693. input,
  2694. commentStart,
  2695. pos + 1,
  2696. doctypeNameStart,
  2697. doctypeNameEnd,
  2698. doctypePublicStart,
  2699. doctypePublicEnd,
  2700. doctypeSystemStart,
  2701. doctypeSystemEnd,
  2702. doctypeForceQuirks
  2703. );
  2704. }
  2705. state = STATE_DATA;
  2706. textStart = nextPos;
  2707. pos = nextPos;
  2708. } else {
  2709. // Anything else
  2710. // This is an unexpected-character-after-doctype-system-identifier parse
  2711. // error. Reconsume in the bogus DOCTYPE state. (This does not set the
  2712. // current DOCTYPE token's force-quirks flag to on.)
  2713. reportError(
  2714. "unexpected-character-after-doctype-system-identifier",
  2715. pos,
  2716. pos + 1,
  2717. "warning"
  2718. );
  2719. state = STATE_BOGUS_DOCTYPE;
  2720. }
  2721. break;
  2722. // https://html.spec.whatwg.org/multipage/parsing.html#bogus-doctype-state
  2723. case STATE_BOGUS_DOCTYPE:
  2724. // Consume the next input character:
  2725. if (cc === CC_GREATER_THAN) {
  2726. // U+003E GREATER-THAN SIGN (>)
  2727. // Switch to the data state. Emit the DOCTYPE token.
  2728. let nextPos = pos + 1;
  2729. if (callbacks.doctype !== undefined) {
  2730. nextPos = callbacks.doctype(
  2731. input,
  2732. commentStart,
  2733. pos + 1,
  2734. doctypeNameStart,
  2735. doctypeNameEnd,
  2736. doctypePublicStart,
  2737. doctypePublicEnd,
  2738. doctypeSystemStart,
  2739. doctypeSystemEnd,
  2740. doctypeForceQuirks
  2741. );
  2742. }
  2743. state = STATE_DATA;
  2744. textStart = nextPos;
  2745. pos = nextPos;
  2746. } else if (cc === CC_NULL) {
  2747. // U+0000 NULL
  2748. // This is an unexpected-null-character parse error. Ignore the character.
  2749. reportError("unexpected-null-character", pos, pos + 1, "warning");
  2750. pos++;
  2751. } else {
  2752. // Anything else
  2753. // Ignore the character.
  2754. pos++;
  2755. }
  2756. break;
  2757. // https://html.spec.whatwg.org/multipage/parsing.html#cdata-section-state
  2758. case STATE_CDATA_SECTION:
  2759. // Consume the next input character:
  2760. // U+005D RIGHT SQUARE BRACKET (])
  2761. // Switch to the CDATA section bracket state.
  2762. if (cc === CC_RIGHT_SQUARE_BRACKET) {
  2763. state = STATE_CDATA_SECTION_BRACKET;
  2764. pos++;
  2765. } else {
  2766. // Anything else
  2767. // Emit the current input character as a character token.
  2768. // Fast-forward to the next `]` (the only code point with its own
  2769. // arc) in one native scan.
  2770. pos = input.indexOf("]", pos + 1);
  2771. if (pos === -1) pos = len;
  2772. }
  2773. break;
  2774. // https://html.spec.whatwg.org/multipage/parsing.html#cdata-section-bracket-state
  2775. case STATE_CDATA_SECTION_BRACKET:
  2776. // Consume the next input character:
  2777. // U+005D RIGHT SQUARE BRACKET (])
  2778. // Switch to the CDATA section end state.
  2779. if (cc === CC_RIGHT_SQUARE_BRACKET) {
  2780. state = STATE_CDATA_SECTION_END;
  2781. pos++;
  2782. } else {
  2783. // Anything else
  2784. // Emit a U+005D RIGHT SQUARE BRACKET character token. Reconsume in the
  2785. // CDATA section state.
  2786. state = STATE_CDATA_SECTION;
  2787. // Reconsume
  2788. }
  2789. break;
  2790. // https://html.spec.whatwg.org/multipage/parsing.html#cdata-section-end-state
  2791. case STATE_CDATA_SECTION_END:
  2792. // Consume the next input character:
  2793. // U+005D RIGHT SQUARE BRACKET (])
  2794. // Emit a U+005D RIGHT SQUARE BRACKET character token.
  2795. if (cc === CC_RIGHT_SQUARE_BRACKET) {
  2796. pos++;
  2797. } else if (cc === CC_GREATER_THAN) {
  2798. // U+003E GREATER-THAN SIGN (>)
  2799. // Switch to the data state. The `]]` just consumed closes the
  2800. // section rather than joining its content.
  2801. commentDataEnd = pos - 2;
  2802. let nextPos = pos + 1;
  2803. if (callbacks.comment !== undefined) {
  2804. nextPos = callbacks.comment(
  2805. input,
  2806. commentStart,
  2807. pos + 1,
  2808. commentDataStart,
  2809. commentDataEnd
  2810. );
  2811. }
  2812. state = STATE_DATA;
  2813. textStart = nextPos;
  2814. pos = nextPos;
  2815. } else {
  2816. // Anything else
  2817. // Emit two U+005D RIGHT SQUARE BRACKET character tokens. Reconsume in the
  2818. // CDATA section state.
  2819. state = STATE_CDATA_SECTION;
  2820. // Reconsume
  2821. }
  2822. break;
  2823. // https://html.spec.whatwg.org/multipage/parsing.html#rcdata-state
  2824. case STATE_RCDATA:
  2825. // Consume the next input character:
  2826. if (cc === CC_AMPERSAND) {
  2827. // U+0026 AMPERSAND (&)
  2828. // Set the return state to the RCDATA state. Switch to the
  2829. // character reference state. (RCDATA processes references;
  2830. // RAWTEXT/script/PLAINTEXT do not.)
  2831. returnState = STATE_RCDATA;
  2832. state = STATE_CHARACTER_REFERENCE;
  2833. pos++;
  2834. } else if (cc === CC_LESS_THAN) {
  2835. // U+003C LESS-THAN SIGN (<)
  2836. // Switch to the RCDATA less-than sign state.
  2837. tagStart = pos;
  2838. state = STATE_RCDATA_LESS_THAN_SIGN;
  2839. pos++;
  2840. } else if (cc === CC_NULL) {
  2841. // U+0000 NULL is an unexpected-null-character parse error.
  2842. reportError("unexpected-null-character", pos, pos + 1, "warning");
  2843. pos++;
  2844. } else {
  2845. // Fast-forward over ordinary RCDATA text (same memoized
  2846. // `indexOf` scheme as the data state) — batches only this
  2847. // state's "anything else" arc; `&` / `<` / NUL keep their own.
  2848. pos++;
  2849. if (nextLt < pos) {
  2850. nextLt = input.indexOf("<", pos);
  2851. if (nextLt === -1) nextLt = len;
  2852. }
  2853. if (nextAmp < pos) {
  2854. nextAmp = input.indexOf("&", pos);
  2855. if (nextAmp === -1) nextAmp = len;
  2856. }
  2857. if (nextNul < pos) {
  2858. nextNul = input.indexOf("\0", pos);
  2859. if (nextNul === -1) nextNul = len;
  2860. }
  2861. pos = Math.min(nextLt, nextAmp, nextNul);
  2862. // Fused: take this state's `<` arc without another dispatch.
  2863. if (pos === nextLt && pos < len) {
  2864. tagStart = pos;
  2865. state = STATE_RCDATA_LESS_THAN_SIGN;
  2866. pos++;
  2867. }
  2868. }
  2869. break;
  2870. // https://html.spec.whatwg.org/multipage/parsing.html#rcdata-less-than-sign-state
  2871. case STATE_RCDATA_LESS_THAN_SIGN:
  2872. // Consume the next input character:
  2873. // U+002F SOLIDUS (/)
  2874. // Switch to the RCDATA end tag open state. (Spec sets a
  2875. // temporary buffer here; we track the would-be content via
  2876. // offset ranges instead.)
  2877. if (cc === CC_SOLIDUS) {
  2878. state = STATE_RCDATA_END_TAG_OPEN;
  2879. pos++;
  2880. } else {
  2881. // Anything else
  2882. // Emit a U+003C LESS-THAN SIGN character token. Reconsume in the RCDATA
  2883. // state.
  2884. state = STATE_RCDATA;
  2885. // Reconsume
  2886. }
  2887. break;
  2888. // https://html.spec.whatwg.org/multipage/parsing.html#rcdata-end-tag-open-state
  2889. case STATE_RCDATA_END_TAG_OPEN:
  2890. // Consume the next input character:
  2891. // ASCII alpha
  2892. // Create a new end tag token, set its tag name to the empty string.
  2893. // Reconsume in the RCDATA end tag name state.
  2894. if (isAsciiAlpha(cc)) {
  2895. tagNameStart = pos;
  2896. state = STATE_RCDATA_END_TAG_NAME;
  2897. // Reconsume
  2898. } else {
  2899. // Anything else
  2900. // Emit a U+003C LESS-THAN SIGN character token and a U+002F SOLIDUS
  2901. // character token. Reconsume in the RCDATA state.
  2902. state = STATE_RCDATA;
  2903. // Reconsume
  2904. }
  2905. break;
  2906. // https://html.spec.whatwg.org/multipage/parsing.html#rcdata-end-tag-name-state
  2907. case STATE_RCDATA_END_TAG_NAME:
  2908. // Consume the next input character:
  2909. // U+0009 CHARACTER TABULATION (tab)
  2910. // U+000A LINE FEED (LF)
  2911. // U+000C FORM FEED (FF)
  2912. // U+0020 SPACE
  2913. // If the current end tag token is an appropriate end tag token, then switch
  2914. // to the before attribute name state. Otherwise, treat it as per the
  2915. // "anything else" entry below.
  2916. if (isSpace(cc)) {
  2917. tagNameEnd = pos;
  2918. if (
  2919. rangeEqualsLowerCase(
  2920. input,
  2921. tagNameStart,
  2922. tagNameEnd,
  2923. lastOpenTagName
  2924. )
  2925. ) {
  2926. flushText(tagStart);
  2927. state = STATE_BEFORE_ATTRIBUTE_NAME;
  2928. pos++;
  2929. } else {
  2930. state = STATE_RCDATA;
  2931. // Reconsume
  2932. }
  2933. } else if (cc === CC_SOLIDUS) {
  2934. // U+002F SOLIDUS (/)
  2935. // If the current end tag token is an appropriate end tag token, then switch
  2936. // to the self-closing start tag state. Otherwise, treat it as per the
  2937. // "anything else" entry below.
  2938. tagNameEnd = pos;
  2939. if (
  2940. rangeEqualsLowerCase(
  2941. input,
  2942. tagNameStart,
  2943. tagNameEnd,
  2944. lastOpenTagName
  2945. )
  2946. ) {
  2947. flushText(tagStart);
  2948. state = STATE_SELF_CLOSING_START_TAG;
  2949. pos++;
  2950. } else {
  2951. state = STATE_RCDATA;
  2952. // Reconsume
  2953. }
  2954. } else if (cc === CC_GREATER_THAN) {
  2955. // U+003E GREATER-THAN SIGN (>)
  2956. // If the current end tag token is an appropriate end tag token, then switch
  2957. // to the data state and emit the current tag token. Otherwise, treat it as
  2958. // per the "anything else" entry below.
  2959. tagNameEnd = pos;
  2960. if (
  2961. rangeEqualsLowerCase(
  2962. input,
  2963. tagNameStart,
  2964. tagNameEnd,
  2965. lastOpenTagName
  2966. )
  2967. ) {
  2968. flushText(tagStart);
  2969. state = STATE_DATA;
  2970. pos = emitCloseTag(pos + 1);
  2971. } else {
  2972. state = STATE_RCDATA;
  2973. // Reconsume
  2974. }
  2975. } else if (isAsciiAlpha(cc)) {
  2976. // ASCII upper alpha / ASCII lower alpha
  2977. // Append the lowercase version of the current input character to the
  2978. // current tag token's tag name. Append the current input character to
  2979. // the temporary buffer.
  2980. // Fused: consume the whole alpha run in one dispatch.
  2981. pos++;
  2982. while (pos < len && isAsciiAlpha(input.charCodeAt(pos))) pos++;
  2983. } else {
  2984. // Anything else
  2985. // Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character
  2986. // token, and a character token for each of the characters in the temporary
  2987. // buffer (in the order they were added to the buffer). Reconsume in the
  2988. // RCDATA state.
  2989. state = STATE_RCDATA;
  2990. // Reconsume
  2991. }
  2992. break;
  2993. // https://html.spec.whatwg.org/multipage/parsing.html#rawtext-state
  2994. case STATE_RAWTEXT:
  2995. // Consume the next input character:
  2996. // U+003C LESS-THAN SIGN (<)
  2997. // Switch to the RAWTEXT less-than sign state.
  2998. if (cc === CC_LESS_THAN) {
  2999. tagStart = pos;
  3000. state = STATE_RAWTEXT_LESS_THAN_SIGN;
  3001. pos++;
  3002. } else if (cc === CC_NULL) {
  3003. // U+0000 NULL is an unexpected-null-character parse error.
  3004. reportError("unexpected-null-character", pos, pos + 1, "warning");
  3005. pos++;
  3006. } else {
  3007. // Fast-forward over ordinary RAWTEXT text (same memoized
  3008. // `indexOf` scheme as the data state) — batches only this
  3009. // state's "anything else" arc; `<` / NUL keep their own.
  3010. pos++;
  3011. if (nextLt < pos) {
  3012. nextLt = input.indexOf("<", pos);
  3013. if (nextLt === -1) nextLt = len;
  3014. }
  3015. if (nextNul < pos) {
  3016. nextNul = input.indexOf("\0", pos);
  3017. if (nextNul === -1) nextNul = len;
  3018. }
  3019. pos = Math.min(nextLt, nextNul);
  3020. // Fused: take this state's `<` arc without another dispatch.
  3021. if (pos === nextLt && pos < len) {
  3022. tagStart = pos;
  3023. state = STATE_RAWTEXT_LESS_THAN_SIGN;
  3024. pos++;
  3025. }
  3026. }
  3027. break;
  3028. // https://html.spec.whatwg.org/multipage/parsing.html#rawtext-less-than-sign-state
  3029. case STATE_RAWTEXT_LESS_THAN_SIGN:
  3030. // Consume the next input character:
  3031. // U+002F SOLIDUS (/)
  3032. // Switch to the RAWTEXT end tag open state. (Spec sets a
  3033. // temporary buffer here; we track via offset ranges instead.)
  3034. if (cc === CC_SOLIDUS) {
  3035. state = STATE_RAWTEXT_END_TAG_OPEN;
  3036. pos++;
  3037. } else {
  3038. // Anything else
  3039. // Emit a U+003C LESS-THAN SIGN character token. Reconsume in the RAWTEXT
  3040. // state.
  3041. state = STATE_RAWTEXT;
  3042. // Reconsume
  3043. }
  3044. break;
  3045. // https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-open-state
  3046. case STATE_RAWTEXT_END_TAG_OPEN:
  3047. // Consume the next input character:
  3048. // ASCII alpha
  3049. // Create a new end tag token, set its tag name to the empty string.
  3050. // Reconsume in the RAWTEXT end tag name state.
  3051. if (isAsciiAlpha(cc)) {
  3052. tagNameStart = pos;
  3053. state = STATE_RAWTEXT_END_TAG_NAME;
  3054. // Reconsume
  3055. } else {
  3056. // Anything else
  3057. // Emit a U+003C LESS-THAN SIGN character token and a U+002F SOLIDUS
  3058. // character token. Reconsume in the RAWTEXT state.
  3059. state = STATE_RAWTEXT;
  3060. // Reconsume
  3061. }
  3062. break;
  3063. // https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-name-state
  3064. case STATE_RAWTEXT_END_TAG_NAME:
  3065. // Consume the next input character:
  3066. // U+0009 CHARACTER TABULATION (tab)
  3067. // U+000A LINE FEED (LF)
  3068. // U+000C FORM FEED (FF)
  3069. // U+0020 SPACE
  3070. // If the current end tag token is an appropriate end tag token, then switch
  3071. // to the before attribute name state. Otherwise, treat it as per the
  3072. // "anything else" entry below.
  3073. if (isSpace(cc)) {
  3074. tagNameEnd = pos;
  3075. if (
  3076. rangeEqualsLowerCase(
  3077. input,
  3078. tagNameStart,
  3079. tagNameEnd,
  3080. lastOpenTagName
  3081. )
  3082. ) {
  3083. flushText(tagStart);
  3084. state = STATE_BEFORE_ATTRIBUTE_NAME;
  3085. pos++;
  3086. } else {
  3087. state = STATE_RAWTEXT;
  3088. }
  3089. } else if (cc === CC_SOLIDUS) {
  3090. // U+002F SOLIDUS (/)
  3091. // If the current end tag token is an appropriate end tag token, then switch
  3092. // to the self-closing start tag state. Otherwise, treat it as per the
  3093. // "anything else" entry below.
  3094. tagNameEnd = pos;
  3095. if (
  3096. rangeEqualsLowerCase(
  3097. input,
  3098. tagNameStart,
  3099. tagNameEnd,
  3100. lastOpenTagName
  3101. )
  3102. ) {
  3103. flushText(tagStart);
  3104. state = STATE_SELF_CLOSING_START_TAG;
  3105. pos++;
  3106. } else {
  3107. state = STATE_RAWTEXT;
  3108. }
  3109. } else if (cc === CC_GREATER_THAN) {
  3110. // U+003E GREATER-THAN SIGN (>)
  3111. // If the current end tag token is an appropriate end tag token, then switch
  3112. // to the data state and emit the current tag token. Otherwise, treat it as
  3113. // per the "anything else" entry below.
  3114. tagNameEnd = pos;
  3115. if (
  3116. rangeEqualsLowerCase(
  3117. input,
  3118. tagNameStart,
  3119. tagNameEnd,
  3120. lastOpenTagName
  3121. )
  3122. ) {
  3123. flushText(tagStart);
  3124. state = STATE_DATA;
  3125. pos = emitCloseTag(pos + 1);
  3126. } else {
  3127. state = STATE_RAWTEXT;
  3128. }
  3129. } else if (isAsciiAlpha(cc)) {
  3130. // ASCII upper alpha / ASCII lower alpha
  3131. // Append the lowercase version of the current input character to the
  3132. // current tag token's tag name. Append the current input character to
  3133. // the temporary buffer.
  3134. // Fused: consume the whole alpha run in one dispatch.
  3135. pos++;
  3136. while (pos < len && isAsciiAlpha(input.charCodeAt(pos))) pos++;
  3137. } else {
  3138. // Anything else
  3139. // Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character
  3140. // token, and a character token for each of the characters in the temporary
  3141. // buffer (in the order they were added to the buffer). Reconsume in the
  3142. // RAWTEXT state.
  3143. state = STATE_RAWTEXT;
  3144. // Reconsume
  3145. }
  3146. break;
  3147. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-state
  3148. case STATE_SCRIPT_DATA:
  3149. // Consume the next input character:
  3150. // U+003C LESS-THAN SIGN (<)
  3151. // Switch to the script data less-than sign state.
  3152. if (cc === CC_LESS_THAN) {
  3153. tagStart = pos;
  3154. state = STATE_SCRIPT_DATA_LESS_THAN_SIGN;
  3155. pos++;
  3156. } else if (cc === CC_NULL) {
  3157. // U+0000 NULL is an unexpected-null-character parse error.
  3158. reportError("unexpected-null-character", pos, pos + 1, "warning");
  3159. pos++;
  3160. } else {
  3161. // Fast-forward over ordinary script-data text (same memoized
  3162. // `indexOf` scheme as the data state) — batches only this
  3163. // state's "anything else" arc; `<` / NUL keep their own.
  3164. pos++;
  3165. if (nextLt < pos) {
  3166. nextLt = input.indexOf("<", pos);
  3167. if (nextLt === -1) nextLt = len;
  3168. }
  3169. if (nextNul < pos) {
  3170. nextNul = input.indexOf("\0", pos);
  3171. if (nextNul === -1) nextNul = len;
  3172. }
  3173. pos = Math.min(nextLt, nextNul);
  3174. // Fused: take this state's `<` arc without another dispatch.
  3175. if (pos === nextLt && pos < len) {
  3176. tagStart = pos;
  3177. state = STATE_SCRIPT_DATA_LESS_THAN_SIGN;
  3178. pos++;
  3179. }
  3180. }
  3181. break;
  3182. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-less-than-sign-state
  3183. case STATE_SCRIPT_DATA_LESS_THAN_SIGN:
  3184. // Consume the next input character:
  3185. // U+002F SOLIDUS (/)
  3186. // Switch to the script data end tag open state. (Spec sets a
  3187. // temporary buffer here; we track via offset ranges instead.)
  3188. if (cc === CC_SOLIDUS) {
  3189. state = STATE_SCRIPT_DATA_END_TAG_OPEN;
  3190. pos++;
  3191. } else if (cc === CC_EXCLAMATION_MARK) {
  3192. // U+0021 EXCLAMATION MARK (!)
  3193. // Switch to the script data escape start state. Emit a U+003C LESS-THAN
  3194. // SIGN character token and a U+0021 EXCLAMATION MARK character token.
  3195. state = STATE_SCRIPT_DATA_ESCAPE_START;
  3196. pos++;
  3197. } else {
  3198. // Anything else
  3199. // Emit a U+003C LESS-THAN SIGN character token. Reconsume in the script
  3200. // data state.
  3201. state = STATE_SCRIPT_DATA;
  3202. // Reconsume
  3203. }
  3204. break;
  3205. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-end-tag-open-state
  3206. case STATE_SCRIPT_DATA_END_TAG_OPEN:
  3207. // Consume the next input character:
  3208. // ASCII alpha
  3209. // Create a new end tag token, set its tag name to the empty string.
  3210. // Reconsume in the script data end tag name state.
  3211. if (isAsciiAlpha(cc)) {
  3212. tagNameStart = pos;
  3213. state = STATE_SCRIPT_DATA_END_TAG_NAME;
  3214. // Reconsume
  3215. } else {
  3216. // Anything else
  3217. // Emit a U+003C LESS-THAN SIGN character token and a U+002F SOLIDUS
  3218. // character token. Reconsume in the script data state.
  3219. state = STATE_SCRIPT_DATA;
  3220. // Reconsume
  3221. }
  3222. break;
  3223. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-end-tag-name-state
  3224. case STATE_SCRIPT_DATA_END_TAG_NAME:
  3225. // Consume the next input character:
  3226. // U+0009 CHARACTER TABULATION (tab)
  3227. // U+000A LINE FEED (LF)
  3228. // U+000C FORM FEED (FF)
  3229. // U+0020 SPACE
  3230. // If the current end tag token is an appropriate end tag token, then switch
  3231. // to the before attribute name state. Otherwise, treat it as per the
  3232. // "anything else" entry below.
  3233. if (isSpace(cc)) {
  3234. tagNameEnd = pos;
  3235. if (
  3236. rangeEqualsLowerCase(
  3237. input,
  3238. tagNameStart,
  3239. tagNameEnd,
  3240. lastOpenTagName
  3241. )
  3242. ) {
  3243. flushText(tagStart);
  3244. state = STATE_BEFORE_ATTRIBUTE_NAME;
  3245. pos++;
  3246. } else {
  3247. state = STATE_SCRIPT_DATA;
  3248. }
  3249. } else if (cc === CC_SOLIDUS) {
  3250. // U+002F SOLIDUS (/)
  3251. // If the current end tag token is an appropriate end tag token, then switch
  3252. // to the self-closing start tag state. Otherwise, treat it as per the
  3253. // "anything else" entry below.
  3254. tagNameEnd = pos;
  3255. if (
  3256. rangeEqualsLowerCase(
  3257. input,
  3258. tagNameStart,
  3259. tagNameEnd,
  3260. lastOpenTagName
  3261. )
  3262. ) {
  3263. flushText(tagStart);
  3264. state = STATE_SELF_CLOSING_START_TAG;
  3265. pos++;
  3266. } else {
  3267. state = STATE_SCRIPT_DATA;
  3268. }
  3269. } else if (cc === CC_GREATER_THAN) {
  3270. // U+003E GREATER-THAN SIGN (>)
  3271. // If the current end tag token is an appropriate end tag token, then switch
  3272. // to the data state and emit the current tag token. Otherwise, treat it as
  3273. // per the "anything else" entry below.
  3274. tagNameEnd = pos;
  3275. if (
  3276. rangeEqualsLowerCase(
  3277. input,
  3278. tagNameStart,
  3279. tagNameEnd,
  3280. lastOpenTagName
  3281. )
  3282. ) {
  3283. flushText(tagStart);
  3284. state = STATE_DATA;
  3285. pos = emitCloseTag(pos + 1);
  3286. } else {
  3287. state = STATE_SCRIPT_DATA;
  3288. }
  3289. } else if (isAsciiAlpha(cc)) {
  3290. // ASCII upper alpha / ASCII lower alpha
  3291. // Fused: consume the whole alpha run in one dispatch.
  3292. pos++;
  3293. while (pos < len && isAsciiAlpha(input.charCodeAt(pos))) pos++;
  3294. } else {
  3295. // Anything else
  3296. // Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character
  3297. // token, and a character token for each of the characters in the temporary
  3298. // buffer (in the order they were added to the buffer). Reconsume in the
  3299. // script data state.
  3300. state = STATE_SCRIPT_DATA;
  3301. // Reconsume
  3302. }
  3303. break;
  3304. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escape-start-state
  3305. case STATE_SCRIPT_DATA_ESCAPE_START:
  3306. // Consume the next input character:
  3307. // U+002D HYPHEN-MINUS (-)
  3308. // Switch to the script data escape start dash state. Emit a U+002D
  3309. // HYPHEN-MINUS character token.
  3310. if (cc === CC_HYPHEN_MINUS) {
  3311. state = STATE_SCRIPT_DATA_ESCAPE_START_DASH;
  3312. pos++;
  3313. } else {
  3314. // Anything else
  3315. // Reconsume in the script data state.
  3316. state = STATE_SCRIPT_DATA;
  3317. // Reconsume
  3318. }
  3319. break;
  3320. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escape-start-dash-state
  3321. case STATE_SCRIPT_DATA_ESCAPE_START_DASH:
  3322. // Consume the next input character:
  3323. // U+002D HYPHEN-MINUS (-)
  3324. // Switch to the script data escaped dash dash state. Emit a U+002D
  3325. // HYPHEN-MINUS character token.
  3326. if (cc === CC_HYPHEN_MINUS) {
  3327. state = STATE_SCRIPT_DATA_ESCAPED_DASH_DASH;
  3328. pos++;
  3329. } else {
  3330. // Anything else
  3331. // Reconsume in the script data state.
  3332. state = STATE_SCRIPT_DATA;
  3333. // Reconsume
  3334. }
  3335. break;
  3336. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-state
  3337. case STATE_SCRIPT_DATA_ESCAPED:
  3338. // Consume the next input character:
  3339. // U+002D HYPHEN-MINUS (-)
  3340. // Switch to the script data escaped dash state. Emit a U+002D HYPHEN-MINUS
  3341. // character token.
  3342. if (cc === CC_HYPHEN_MINUS) {
  3343. state = STATE_SCRIPT_DATA_ESCAPED_DASH;
  3344. pos++;
  3345. } else if (cc === CC_LESS_THAN) {
  3346. // U+003C LESS-THAN SIGN (<)
  3347. // Switch to the script data escaped less-than sign state.
  3348. tagStart = pos;
  3349. state = STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN;
  3350. pos++;
  3351. } else {
  3352. // Anything else
  3353. // Emit the current input character as a character token.
  3354. // U+0000 NULL is an unexpected-null-character parse error.
  3355. if (cc === CC_NULL) {
  3356. reportError("unexpected-null-character", pos, pos + 1, "warning");
  3357. }
  3358. // Fast-forward ordinary escaped script text without re-entering
  3359. // the state switch; stop on the significant code points above.
  3360. pos++;
  3361. while (pos < len) {
  3362. const c2 = input.charCodeAt(pos);
  3363. if (
  3364. c2 === CC_HYPHEN_MINUS ||
  3365. c2 === CC_LESS_THAN ||
  3366. c2 === CC_NULL
  3367. ) {
  3368. break;
  3369. }
  3370. pos++;
  3371. }
  3372. }
  3373. break;
  3374. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-dash-state
  3375. case STATE_SCRIPT_DATA_ESCAPED_DASH:
  3376. // Consume the next input character:
  3377. // U+002D HYPHEN-MINUS (-)
  3378. // Switch to the script data escaped dash dash state. Emit a U+002D
  3379. // HYPHEN-MINUS character token.
  3380. if (cc === CC_HYPHEN_MINUS) {
  3381. state = STATE_SCRIPT_DATA_ESCAPED_DASH_DASH;
  3382. pos++;
  3383. } else if (cc === CC_LESS_THAN) {
  3384. // U+003C LESS-THAN SIGN (<)
  3385. // Switch to the script data escaped less-than sign state.
  3386. tagStart = pos;
  3387. state = STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN;
  3388. pos++;
  3389. } else {
  3390. // Anything else
  3391. // Switch to the script data escaped state. Emit the current input character
  3392. // as a character token. U+0000 NULL is an unexpected-null-character error.
  3393. if (cc === CC_NULL) {
  3394. reportError("unexpected-null-character", pos, pos + 1, "warning");
  3395. }
  3396. state = STATE_SCRIPT_DATA_ESCAPED;
  3397. pos++;
  3398. }
  3399. break;
  3400. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-dash-dash-state
  3401. case STATE_SCRIPT_DATA_ESCAPED_DASH_DASH:
  3402. // Consume the next input character:
  3403. // U+002D HYPHEN-MINUS (-)
  3404. // Emit a U+002D HYPHEN-MINUS character token.
  3405. if (cc === CC_HYPHEN_MINUS) {
  3406. pos++;
  3407. } else if (cc === CC_LESS_THAN) {
  3408. // U+003C LESS-THAN SIGN (<)
  3409. // Switch to the script data escaped less-than sign state.
  3410. tagStart = pos;
  3411. state = STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN;
  3412. pos++;
  3413. } else if (cc === CC_GREATER_THAN) {
  3414. // U+003E GREATER-THAN SIGN (>)
  3415. // Switch to the script data state. Emit a U+003E GREATER-THAN SIGN
  3416. // character token.
  3417. state = STATE_SCRIPT_DATA;
  3418. pos++;
  3419. } else {
  3420. // Anything else
  3421. // Switch to the script data escaped state. Emit the current input character
  3422. // as a character token. U+0000 NULL is an unexpected-null-character error.
  3423. if (cc === CC_NULL) {
  3424. reportError("unexpected-null-character", pos, pos + 1, "warning");
  3425. }
  3426. state = STATE_SCRIPT_DATA_ESCAPED;
  3427. pos++;
  3428. }
  3429. break;
  3430. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-less-than-sign-state
  3431. case STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN:
  3432. // Consume the next input character:
  3433. // U+002F SOLIDUS (/)
  3434. // Switch to the script data escaped end tag open state.
  3435. // (Spec sets a temporary buffer; we track via offset ranges.)
  3436. if (cc === CC_SOLIDUS) {
  3437. state = STATE_SCRIPT_DATA_ESCAPED_END_TAG_OPEN;
  3438. pos++;
  3439. } else if (isAsciiAlpha(cc)) {
  3440. // ASCII alpha
  3441. // Set the temporary buffer to the empty string. Emit a U+003C LESS-THAN
  3442. // SIGN character token. Reconsume in the script data double escape start
  3443. // state.
  3444. scriptMatch = 0;
  3445. state = STATE_SCRIPT_DATA_DOUBLE_ESCAPE_START;
  3446. // Reconsume
  3447. } else {
  3448. // Anything else
  3449. // Emit a U+003C LESS-THAN SIGN character token. Reconsume in the script
  3450. // data escaped state.
  3451. state = STATE_SCRIPT_DATA_ESCAPED;
  3452. // Reconsume
  3453. }
  3454. break;
  3455. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-end-tag-open-state
  3456. case STATE_SCRIPT_DATA_ESCAPED_END_TAG_OPEN:
  3457. // Consume the next input character:
  3458. // ASCII alpha
  3459. // Create a new end tag token, set its tag name to the empty string.
  3460. // Reconsume in the script data escaped end tag name state.
  3461. if (isAsciiAlpha(cc)) {
  3462. tagNameStart = pos;
  3463. state = STATE_SCRIPT_DATA_ESCAPED_END_TAG_NAME;
  3464. // Reconsume
  3465. } else {
  3466. // Anything else
  3467. // Emit a U+003C LESS-THAN SIGN character token and a U+002F SOLIDUS
  3468. // character token. Reconsume in the script data escaped state.
  3469. state = STATE_SCRIPT_DATA_ESCAPED;
  3470. // Reconsume
  3471. }
  3472. break;
  3473. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-escaped-end-tag-name-state
  3474. case STATE_SCRIPT_DATA_ESCAPED_END_TAG_NAME:
  3475. // Consume the next input character:
  3476. // U+0009 CHARACTER TABULATION (tab)
  3477. // U+000A LINE FEED (LF)
  3478. // U+000C FORM FEED (FF)
  3479. // U+0020 SPACE
  3480. // If the current end tag token is an appropriate end tag token, then switch
  3481. // to the before attribute name state. Otherwise, treat it as per the
  3482. // "anything else" entry below.
  3483. if (isSpace(cc)) {
  3484. tagNameEnd = pos;
  3485. if (
  3486. rangeEqualsLowerCase(
  3487. input,
  3488. tagNameStart,
  3489. tagNameEnd,
  3490. lastOpenTagName
  3491. )
  3492. ) {
  3493. flushText(tagStart);
  3494. state = STATE_BEFORE_ATTRIBUTE_NAME;
  3495. pos++;
  3496. } else {
  3497. state = STATE_SCRIPT_DATA_ESCAPED;
  3498. }
  3499. } else if (cc === CC_SOLIDUS) {
  3500. // U+002F SOLIDUS (/)
  3501. // If the current end tag token is an appropriate end tag token, then switch
  3502. // to the self-closing start tag state. Otherwise, treat it as per the
  3503. // "anything else" entry below.
  3504. tagNameEnd = pos;
  3505. if (
  3506. rangeEqualsLowerCase(
  3507. input,
  3508. tagNameStart,
  3509. tagNameEnd,
  3510. lastOpenTagName
  3511. )
  3512. ) {
  3513. flushText(tagStart);
  3514. state = STATE_SELF_CLOSING_START_TAG;
  3515. pos++;
  3516. } else {
  3517. state = STATE_SCRIPT_DATA_ESCAPED;
  3518. }
  3519. } else if (cc === CC_GREATER_THAN) {
  3520. // U+003E GREATER-THAN SIGN (>)
  3521. // If the current end tag token is an appropriate end tag token, then switch
  3522. // to the data state and emit the current tag token. Otherwise, treat it as
  3523. // per the "anything else" entry below.
  3524. tagNameEnd = pos;
  3525. if (
  3526. rangeEqualsLowerCase(
  3527. input,
  3528. tagNameStart,
  3529. tagNameEnd,
  3530. lastOpenTagName
  3531. )
  3532. ) {
  3533. flushText(tagStart);
  3534. state = STATE_DATA;
  3535. pos = emitCloseTag(pos + 1);
  3536. } else {
  3537. state = STATE_SCRIPT_DATA_ESCAPED;
  3538. }
  3539. } else if (isAsciiAlpha(cc)) {
  3540. // ASCII upper alpha / ASCII lower alpha
  3541. // Fused: consume the whole alpha run in one dispatch.
  3542. pos++;
  3543. while (pos < len && isAsciiAlpha(input.charCodeAt(pos))) pos++;
  3544. } else {
  3545. // Anything else
  3546. // Emit a U+003C LESS-THAN SIGN character token, a U+002F SOLIDUS character
  3547. // token, and a character token for each of the characters in the temporary
  3548. // buffer (in the order they were added to the buffer). Reconsume in the
  3549. // script data escaped state.
  3550. state = STATE_SCRIPT_DATA_ESCAPED;
  3551. // Reconsume
  3552. }
  3553. break;
  3554. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escape-start-state
  3555. case STATE_SCRIPT_DATA_DOUBLE_ESCAPE_START:
  3556. // Consume the next input character:
  3557. // U+0009 CHARACTER TABULATION (tab)
  3558. // U+000A LINE FEED (LF)
  3559. // U+000C FORM FEED (FF)
  3560. // U+0020 SPACE
  3561. // U+002F SOLIDUS (/)
  3562. // U+003E GREATER-THAN SIGN (>)
  3563. // If the temporary buffer is the string "script", then switch to the script
  3564. // data double escaped state. Otherwise, switch to the script data escaped
  3565. // state. Emit the current input character as a character token.
  3566. if (isSpace(cc) || cc === CC_SOLIDUS || cc === CC_GREATER_THAN) {
  3567. state =
  3568. scriptMatch === 6
  3569. ? STATE_SCRIPT_DATA_DOUBLE_ESCAPED
  3570. : STATE_SCRIPT_DATA_ESCAPED;
  3571. pos++;
  3572. } else if (isAsciiUpperAlpha(cc) || isAsciiLowerAlpha(cc)) {
  3573. // ASCII alpha — advance the `"script"` match counter if the
  3574. // lowercase form matches the next expected char, otherwise
  3575. // snap to the sentinel so further chars can't revive a
  3576. // match. No buffer allocation.
  3577. const lower = isAsciiUpperAlpha(cc) ? cc + 0x20 : cc;
  3578. if (scriptMatch < 6 && lower === "script".charCodeAt(scriptMatch)) {
  3579. scriptMatch++;
  3580. } else {
  3581. scriptMatch = 7;
  3582. }
  3583. pos++;
  3584. } else {
  3585. // Anything else
  3586. // Reconsume in the script data escaped state.
  3587. state = STATE_SCRIPT_DATA_ESCAPED;
  3588. // Reconsume
  3589. }
  3590. break;
  3591. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-state
  3592. case STATE_SCRIPT_DATA_DOUBLE_ESCAPED:
  3593. // Consume the next input character:
  3594. // U+002D HYPHEN-MINUS (-)
  3595. // Switch to the script data double escaped dash state. Emit a U+002D
  3596. // HYPHEN-MINUS character token.
  3597. if (cc === CC_HYPHEN_MINUS) {
  3598. state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH;
  3599. pos++;
  3600. } else if (cc === CC_LESS_THAN) {
  3601. // U+003C LESS-THAN SIGN (<)
  3602. // Switch to the script data double escaped less-than sign state. Emit a
  3603. // U+003C LESS-THAN SIGN character token.
  3604. state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN;
  3605. pos++;
  3606. } else {
  3607. // Anything else
  3608. // Emit the current input character as a character token.
  3609. // U+0000 NULL is an unexpected-null-character parse error.
  3610. if (cc === CC_NULL) {
  3611. reportError("unexpected-null-character", pos, pos + 1, "warning");
  3612. }
  3613. // Fast-forward ordinary double-escaped script text without re-entering
  3614. // the state switch; stop on the significant code points above.
  3615. pos++;
  3616. while (pos < len) {
  3617. const c2 = input.charCodeAt(pos);
  3618. if (
  3619. c2 === CC_HYPHEN_MINUS ||
  3620. c2 === CC_LESS_THAN ||
  3621. c2 === CC_NULL
  3622. ) {
  3623. break;
  3624. }
  3625. pos++;
  3626. }
  3627. }
  3628. break;
  3629. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-dash-state
  3630. case STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH:
  3631. // Consume the next input character:
  3632. // U+002D HYPHEN-MINUS (-)
  3633. // Switch to the script data double escaped dash dash state. Emit a U+002D
  3634. // HYPHEN-MINUS character token.
  3635. if (cc === CC_HYPHEN_MINUS) {
  3636. state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH_DASH;
  3637. pos++;
  3638. } else if (cc === CC_LESS_THAN) {
  3639. // U+003C LESS-THAN SIGN (<)
  3640. // Switch to the script data double escaped less-than sign state. Emit a
  3641. // U+003C LESS-THAN SIGN character token.
  3642. state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN;
  3643. pos++;
  3644. } else {
  3645. // Anything else
  3646. // Switch to the script data double escaped state. Emit the current input
  3647. // character as a character token. NULL is unexpected-null-character.
  3648. if (cc === CC_NULL) {
  3649. reportError("unexpected-null-character", pos, pos + 1, "warning");
  3650. }
  3651. state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED;
  3652. pos++;
  3653. }
  3654. break;
  3655. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-dash-dash-state
  3656. case STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH_DASH:
  3657. // Consume the next input character:
  3658. // U+002D HYPHEN-MINUS (-)
  3659. // Emit a U+002D HYPHEN-MINUS character token.
  3660. if (cc === CC_HYPHEN_MINUS) {
  3661. pos++;
  3662. } else if (cc === CC_LESS_THAN) {
  3663. // U+003C LESS-THAN SIGN (<)
  3664. // Switch to the script data double escaped less-than sign state. Emit a
  3665. // U+003C LESS-THAN SIGN character token.
  3666. state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN;
  3667. pos++;
  3668. } else if (cc === CC_GREATER_THAN) {
  3669. // U+003E GREATER-THAN SIGN (>)
  3670. // Switch to the script data state. Emit a U+003E GREATER-THAN SIGN
  3671. // character token.
  3672. state = STATE_SCRIPT_DATA;
  3673. pos++;
  3674. } else {
  3675. // Anything else
  3676. // Switch to the script data double escaped state. Emit the current input
  3677. // character as a character token. NULL is unexpected-null-character.
  3678. if (cc === CC_NULL) {
  3679. reportError("unexpected-null-character", pos, pos + 1, "warning");
  3680. }
  3681. state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED;
  3682. pos++;
  3683. }
  3684. break;
  3685. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escaped-less-than-sign-state
  3686. case STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN:
  3687. // Consume the next input character:
  3688. // U+002F SOLIDUS (/)
  3689. // Set the temporary buffer to the empty string. Switch to the script data
  3690. // double escape end state. Emit a U+002F SOLIDUS character token.
  3691. if (cc === CC_SOLIDUS) {
  3692. scriptMatch = 0;
  3693. state = STATE_SCRIPT_DATA_DOUBLE_ESCAPE_END;
  3694. pos++;
  3695. } else {
  3696. // Anything else
  3697. // Reconsume in the script data double escaped state.
  3698. state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED;
  3699. // Reconsume
  3700. }
  3701. break;
  3702. // https://html.spec.whatwg.org/multipage/parsing.html#script-data-double-escape-end-state
  3703. case STATE_SCRIPT_DATA_DOUBLE_ESCAPE_END:
  3704. // Consume the next input character:
  3705. // U+0009 CHARACTER TABULATION (tab)
  3706. // U+000A LINE FEED (LF)
  3707. // U+000C FORM FEED (FF)
  3708. // U+0020 SPACE
  3709. // U+002F SOLIDUS (/)
  3710. // U+003E GREATER-THAN SIGN (>)
  3711. // If the temporary buffer is the string "script", then switch to the script
  3712. // data escaped state. Otherwise, switch to the script data double escaped
  3713. // state. Emit the current input character as a character token.
  3714. if (isSpace(cc) || cc === CC_SOLIDUS || cc === CC_GREATER_THAN) {
  3715. state =
  3716. scriptMatch === 6
  3717. ? STATE_SCRIPT_DATA_ESCAPED
  3718. : STATE_SCRIPT_DATA_DOUBLE_ESCAPED;
  3719. pos++;
  3720. } else if (isAsciiUpperAlpha(cc) || isAsciiLowerAlpha(cc)) {
  3721. // ASCII alpha — advance the `"script"` match counter if the
  3722. // lowercase form matches the next expected char, otherwise
  3723. // snap to the sentinel so further chars can't revive a
  3724. // match. No buffer allocation.
  3725. const lower = isAsciiUpperAlpha(cc) ? cc + 0x20 : cc;
  3726. if (scriptMatch < 6 && lower === "script".charCodeAt(scriptMatch)) {
  3727. scriptMatch++;
  3728. } else {
  3729. scriptMatch = 7;
  3730. }
  3731. pos++;
  3732. } else {
  3733. // Anything else
  3734. // Reconsume in the script data double escaped state.
  3735. state = STATE_SCRIPT_DATA_DOUBLE_ESCAPED;
  3736. // Reconsume
  3737. }
  3738. break;
  3739. // https://html.spec.whatwg.org/multipage/parsing.html#plaintext-state
  3740. case STATE_PLAINTEXT:
  3741. // Consume the next input character:
  3742. // U+0000 NULL is an unexpected-null-character parse error.
  3743. // Anything else: emit the current input character.
  3744. if (cc === CC_NULL) {
  3745. reportError("unexpected-null-character", pos, pos + 1, "warning");
  3746. pos++;
  3747. } else {
  3748. // Fast-forward to the next NULL (or EOF) in one native scan.
  3749. pos++;
  3750. const nul = input.indexOf("\0", pos);
  3751. pos = nul === -1 ? len : nul;
  3752. }
  3753. break;
  3754. // https://html.spec.whatwg.org/multipage/parsing.html#character-reference-state
  3755. case STATE_CHARACTER_REFERENCE:
  3756. // Set the temporary buffer to the empty string. Append a U+0026
  3757. // AMPERSAND (&) character to the temporary buffer.
  3758. // `charRefStart` points at that `&` (one before the current pos).
  3759. charRefStart = pos - 1;
  3760. // Consume the next input character:
  3761. if (isAsciiAlphanumeric(cc)) {
  3762. // ASCII alphanumeric
  3763. // Reconsume in the named character reference state.
  3764. state = STATE_NAMED_CHARACTER_REFERENCE;
  3765. // Reconsume
  3766. } else if (cc === CC_NUMBER_SIGN) {
  3767. // U+0023 NUMBER SIGN (#)
  3768. // Append the current input character to the temporary buffer.
  3769. // Set the character reference code to zero. Switch to the
  3770. // numeric character reference state.
  3771. charRefCode = 0;
  3772. state = STATE_NUMERIC_CHARACTER_REFERENCE;
  3773. pos++;
  3774. } else {
  3775. // Anything else
  3776. // Flush code points consumed as a character reference.
  3777. // Reconsume in the return state.
  3778. state = returnState;
  3779. // Reconsume
  3780. }
  3781. break;
  3782. // https://html.spec.whatwg.org/multipage/parsing.html#named-character-reference-state
  3783. case STATE_NAMED_CHARACTER_REFERENCE: {
  3784. // Consume the maximum number of characters possible where the
  3785. // consumed characters are one of the identifiers in the first
  3786. // column of the named character references table.
  3787. //
  3788. // We measure the longest run of ASCII alphanumeric characters
  3789. // (capped at MAX_ENTITY_NAME_LEN - 1 since the optional `;` is
  3790. // handled separately), then walk that run from longest to
  3791. // shortest looking for the first prefix that exists in the
  3792. // entity table (with a trailing `;` if present, otherwise the
  3793. // legacy bare form).
  3794. let runLen = 0;
  3795. while (
  3796. pos + runLen < len &&
  3797. isAsciiAlphanumeric(input.charCodeAt(pos + runLen)) &&
  3798. runLen < MAX_ENTITY_NAME_LEN - 1
  3799. ) {
  3800. runLen++;
  3801. }
  3802. const hasSemicolon =
  3803. pos + runLen < len && input.charCodeAt(pos + runLen) === CC_SEMICOLON;
  3804. namedEntityConsumed = 0;
  3805. let matchedWithSemicolon = false;
  3806. // Try the full run with its trailing `;` first — the overwhelmingly
  3807. // common case (`&amp;`, `&nbsp;`, …) then needs exactly one slice.
  3808. if (hasSemicolon && runLen > 0) {
  3809. const withSemicolon = input.slice(pos, pos + runLen + 1);
  3810. if (HTML_ENTITIES[withSemicolon] !== undefined) {
  3811. namedEntityConsumed = runLen + 1;
  3812. matchedWithSemicolon = true;
  3813. }
  3814. }
  3815. if (namedEntityConsumed === 0) {
  3816. // Slice the candidate run once; prefixes come from this short
  3817. // string instead of re-slicing the input per length.
  3818. const run = input.slice(pos, pos + runLen);
  3819. for (let n = runLen; n > 0; n--) {
  3820. const bare = n === runLen ? run : run.slice(0, n);
  3821. if (HTML_ENTITIES[bare] !== undefined) {
  3822. namedEntityConsumed = n;
  3823. break;
  3824. }
  3825. }
  3826. }
  3827. if (namedEntityConsumed > 0) {
  3828. // A legacy match without a trailing `;` is a
  3829. // missing-semicolon-after-character-reference parse error,
  3830. // except for the spec's historical attribute rule: when
  3831. // consumed in an attribute value and the next char is `=` or
  3832. // ASCII alphanumeric, the reference is left undecoded silently.
  3833. if (!matchedWithSemicolon) {
  3834. const next = input.charCodeAt(pos + namedEntityConsumed);
  3835. const inAttribute =
  3836. returnState === STATE_ATTRIBUTE_VALUE_DOUBLE_QUOTED ||
  3837. returnState === STATE_ATTRIBUTE_VALUE_SINGLE_QUOTED ||
  3838. returnState === STATE_ATTRIBUTE_VALUE_UNQUOTED;
  3839. if (!(
  3840. inAttribute &&
  3841. (next === CC_EQUALS || isAsciiAlphanumeric(next))
  3842. )) {
  3843. reportError(
  3844. "missing-semicolon-after-character-reference",
  3845. pos + namedEntityConsumed,
  3846. pos + namedEntityConsumed + 1,
  3847. "warning"
  3848. );
  3849. }
  3850. }
  3851. pos += namedEntityConsumed;
  3852. state = returnState;
  3853. } else {
  3854. // No match — flush code points consumed as a character
  3855. // reference. Switch to the ambiguous ampersand state.
  3856. state = STATE_AMBIGUOUS_AMPERSAND;
  3857. }
  3858. break;
  3859. }
  3860. // https://html.spec.whatwg.org/multipage/parsing.html#ambiguous-ampersand-state
  3861. case STATE_AMBIGUOUS_AMPERSAND:
  3862. // Consume the next input character:
  3863. if (isAsciiAlphanumeric(cc)) {
  3864. // ASCII alphanumeric
  3865. // If the character reference was consumed as part of an
  3866. // attribute, then append the current input character to the
  3867. // current attribute's value. Otherwise, emit the current
  3868. // input character as a character token.
  3869. pos++;
  3870. } else if (cc === CC_SEMICOLON) {
  3871. // U+003B SEMICOLON (;)
  3872. // This is an unknown-named-character-reference parse error.
  3873. // Reconsume in the return state.
  3874. reportError(
  3875. "unknown-named-character-reference",
  3876. pos,
  3877. pos + 1,
  3878. "warning"
  3879. );
  3880. state = returnState;
  3881. // Reconsume
  3882. } else {
  3883. // Anything else
  3884. // Reconsume in the return state.
  3885. state = returnState;
  3886. // Reconsume
  3887. }
  3888. break;
  3889. // https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-state
  3890. case STATE_NUMERIC_CHARACTER_REFERENCE:
  3891. // Set the character reference code to zero (0).
  3892. // Consume the next input character:
  3893. if (cc === 0x78 || cc === 0x58) {
  3894. // U+0078 LATIN SMALL LETTER X
  3895. // U+0058 LATIN CAPITAL LETTER X
  3896. // Append the current input character to the temporary
  3897. // buffer. Switch to the hexadecimal character reference
  3898. // start state.
  3899. state = STATE_HEXADECIMAL_CHARACTER_REFERENCE_START;
  3900. pos++;
  3901. } else {
  3902. // Anything else
  3903. // Reconsume in the decimal character reference start state.
  3904. state = STATE_DECIMAL_CHARACTER_REFERENCE_START;
  3905. // Reconsume
  3906. }
  3907. break;
  3908. // https://html.spec.whatwg.org/multipage/parsing.html#hexadecimal-character-reference-start-state
  3909. case STATE_HEXADECIMAL_CHARACTER_REFERENCE_START:
  3910. // Consume the next input character:
  3911. // ASCII hex digit: reconsume in the hexadecimal character reference state.
  3912. // Anything else: absence-of-digits-in-numeric-character-reference parse
  3913. // error. Flush code points consumed as a character reference. Reconsume
  3914. // in the return state.
  3915. if (isAsciiHexDigit(cc)) {
  3916. state = STATE_HEXADECIMAL_CHARACTER_REFERENCE;
  3917. } else {
  3918. reportError(
  3919. "absence-of-digits-in-numeric-character-reference",
  3920. pos,
  3921. pos + 1,
  3922. "warning"
  3923. );
  3924. state = returnState;
  3925. }
  3926. // Reconsume
  3927. break;
  3928. // https://html.spec.whatwg.org/multipage/parsing.html#decimal-character-reference-start-state
  3929. case STATE_DECIMAL_CHARACTER_REFERENCE_START:
  3930. // Consume the next input character:
  3931. // ASCII digit: reconsume in the decimal character reference state.
  3932. // Anything else: absence-of-digits-in-numeric-character-reference parse
  3933. // error. Flush code points consumed as a character reference. Reconsume
  3934. // in the return state.
  3935. if (isAsciiDigit(cc)) {
  3936. state = STATE_DECIMAL_CHARACTER_REFERENCE;
  3937. } else {
  3938. reportError(
  3939. "absence-of-digits-in-numeric-character-reference",
  3940. pos,
  3941. pos + 1,
  3942. "warning"
  3943. );
  3944. state = returnState;
  3945. }
  3946. // Reconsume
  3947. break;
  3948. // https://html.spec.whatwg.org/multipage/parsing.html#hexadecimal-character-reference-state
  3949. case STATE_HEXADECIMAL_CHARACTER_REFERENCE:
  3950. // Consume the next input character:
  3951. if (isAsciiHexDigit(cc)) {
  3952. // ASCII digit / upper hex / lower hex
  3953. // Multiply the character reference code by 16. Add a numeric
  3954. // version of the current input character to the character
  3955. // reference code. Stop accumulating once past the Unicode
  3956. // range so the value can't overflow (still flags as
  3957. // outside-range at the end).
  3958. if (charRefCode < 0x110000) {
  3959. const v = cc <= 0x39 ? cc - 0x30 : (cc | 0x20) - 0x61 + 10;
  3960. charRefCode = charRefCode * 16 + v;
  3961. }
  3962. pos++;
  3963. } else if (cc === CC_SEMICOLON) {
  3964. // U+003B SEMICOLON
  3965. // Switch to the numeric character reference end state.
  3966. state = STATE_NUMERIC_CHARACTER_REFERENCE_END;
  3967. pos++;
  3968. } else {
  3969. // Anything else
  3970. // This is a missing-semicolon-after-character-reference
  3971. // parse error. Reconsume in the numeric character reference
  3972. // end state.
  3973. reportError(
  3974. "missing-semicolon-after-character-reference",
  3975. pos,
  3976. pos + 1,
  3977. "warning"
  3978. );
  3979. state = STATE_NUMERIC_CHARACTER_REFERENCE_END;
  3980. // Reconsume
  3981. }
  3982. break;
  3983. // https://html.spec.whatwg.org/multipage/parsing.html#decimal-character-reference-state
  3984. case STATE_DECIMAL_CHARACTER_REFERENCE:
  3985. // Consume the next input character:
  3986. if (isAsciiDigit(cc)) {
  3987. // ASCII digit
  3988. // Multiply the character reference code by 10. Add a numeric
  3989. // version of the current input character (subtract 0x0030
  3990. // from the character's code point) to the character reference
  3991. // code. Stop accumulating once past the Unicode range so the
  3992. // value can't overflow (still flags as outside-range).
  3993. if (charRefCode < 0x110000) {
  3994. charRefCode = charRefCode * 10 + (cc - 0x30);
  3995. }
  3996. pos++;
  3997. } else if (cc === CC_SEMICOLON) {
  3998. // U+003B SEMICOLON
  3999. // Switch to the numeric character reference end state.
  4000. state = STATE_NUMERIC_CHARACTER_REFERENCE_END;
  4001. pos++;
  4002. } else {
  4003. // Anything else
  4004. // This is a missing-semicolon-after-character-reference
  4005. // parse error. Reconsume in the numeric character reference
  4006. // end state.
  4007. reportError(
  4008. "missing-semicolon-after-character-reference",
  4009. pos,
  4010. pos + 1,
  4011. "warning"
  4012. );
  4013. state = STATE_NUMERIC_CHARACTER_REFERENCE_END;
  4014. // Reconsume
  4015. }
  4016. break;
  4017. // https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
  4018. case STATE_NUMERIC_CHARACTER_REFERENCE_END:
  4019. // Check the character reference code and report the matching
  4020. // WHATWG validation parse error.
  4021. validateNumericReference(pos);
  4022. // Flush code points consumed as a character reference.
  4023. // Switch to the return state.
  4024. state = returnState;
  4025. // Reconsume
  4026. break;
  4027. /* istanbul ignore next -- @preserve: defensive fallback, all states are explicit above */
  4028. default:
  4029. pos++;
  4030. }
  4031. }
  4032. // Handle EOF in non-data states per the WHATWG spec.
  4033. //
  4034. // Each in-progress comment / doctype / cdata / tag emits its partial
  4035. // token range plus a corresponding `eof-in-X` parse error. Severity is
  4036. // `"error"` because the emitted token offset range is incomplete (missing
  4037. // trailing `-->`, `>`, `]]>`, etc.). For data / `<` / `</` / `<!`-only
  4038. // inputs we emit `eof-before-tag-name` and fall through to flush the
  4039. // pending text span (which still contains the lone `<`).
  4040. // EOF inside a character-reference state: run the end-of-reference
  4041. // processing (numeric end states never run when the reference ends at
  4042. // EOF), then resume in the return state for the branches below.
  4043. if (
  4044. state >= STATE_CHARACTER_REFERENCE &&
  4045. state <= STATE_NUMERIC_CHARACTER_REFERENCE_END
  4046. ) {
  4047. if (
  4048. state === STATE_NUMERIC_CHARACTER_REFERENCE ||
  4049. state === STATE_HEXADECIMAL_CHARACTER_REFERENCE_START ||
  4050. state === STATE_DECIMAL_CHARACTER_REFERENCE_START
  4051. ) {
  4052. // No digits before EOF.
  4053. reportError(
  4054. "absence-of-digits-in-numeric-character-reference",
  4055. len,
  4056. len,
  4057. "warning"
  4058. );
  4059. } else if (
  4060. state === STATE_HEXADECIMAL_CHARACTER_REFERENCE ||
  4061. state === STATE_DECIMAL_CHARACTER_REFERENCE
  4062. ) {
  4063. // Digits but no closing `;` before EOF.
  4064. reportError(
  4065. "missing-semicolon-after-character-reference",
  4066. len,
  4067. len,
  4068. "warning"
  4069. );
  4070. validateNumericReference(len);
  4071. } else if (state === STATE_NUMERIC_CHARACTER_REFERENCE_END) {
  4072. validateNumericReference(len);
  4073. }
  4074. state = returnState;
  4075. }
  4076. if (
  4077. (state >= STATE_TAG_NAME && state <= STATE_SELF_CLOSING_START_TAG) ||
  4078. state === STATE_RCDATA_END_TAG_NAME ||
  4079. state === STATE_RAWTEXT_END_TAG_NAME ||
  4080. state === STATE_SCRIPT_DATA_END_TAG_NAME ||
  4081. state === STATE_SCRIPT_DATA_ESCAPED_END_TAG_NAME
  4082. ) {
  4083. // EOF mid-tag — emit the partial open/close tag at EOF so the
  4084. // consumer still sees the tag. This is a deliberate deviation
  4085. // from the spec's per-character emission model: rather than
  4086. // dropping the in-progress tag, we emit its offset range up to EOF.
  4087. reportError("eof-in-tag", len, len, "error");
  4088. // If we hit EOF mid-attribute-name, the name runs to EOF. Set
  4089. // attributeNameEnd here so the emitted attribute range is valid.
  4090. if (state === STATE_ATTRIBUTE_NAME && attributeNameStart !== -1) {
  4091. attributeNameEnd = len;
  4092. }
  4093. if (attributeNameStart !== -1) emitAttribute(len);
  4094. // If we hit EOF before the tag-name end was recorded, the name runs
  4095. // to EOF. `tagNameEnd` may carry over from a previously emitted tag,
  4096. // so reset it whenever it's missing or stale (less than `tagNameStart`)
  4097. // — covers `<div` open-tag EOFs as well as `<title>x</tit` and other
  4098. // content-mode end-tag-name EOFs.
  4099. if (tagNameStart !== -1 && tagNameEnd < tagNameStart) {
  4100. tagNameEnd = len;
  4101. }
  4102. flushText(tagStart);
  4103. pos =
  4104. input.charCodeAt(tagStart + 1) === CC_SOLIDUS
  4105. ? emitCloseTag(len)
  4106. : emitOpenTag(len, false);
  4107. } else if (
  4108. (state >= STATE_COMMENT_START && state <= STATE_BOGUS_COMMENT) ||
  4109. (state >= STATE_COMMENT_LESS_THAN_SIGN &&
  4110. state <= STATE_COMMENT_LESS_THAN_SIGN_BANG_DASH_DASH) ||
  4111. state === STATE_MARKUP_DECLARATION_OPEN
  4112. ) {
  4113. // EOF in markup-declaration-open takes the spec's "anything else"
  4114. // branch: incorrectly-opened-comment, then a bogus comment (which has
  4115. // no EOF error of its own). Bogus comments at EOF are likewise normal.
  4116. if (state === STATE_MARKUP_DECLARATION_OPEN) {
  4117. reportError("incorrectly-opened-comment", commentStart, len, "warning");
  4118. // The bogus comment this becomes has nothing left to consume.
  4119. commentDataStart = len;
  4120. commentDataEnd = len;
  4121. } else if (state !== STATE_BOGUS_COMMENT) {
  4122. reportError("eof-in-comment", len, len, "error");
  4123. }
  4124. if (callbacks.comment !== undefined) {
  4125. pos = callbacks.comment(
  4126. input,
  4127. commentStart,
  4128. len,
  4129. commentDataStart,
  4130. commentDataEnd
  4131. );
  4132. }
  4133. } else if (state >= STATE_CDATA_SECTION && state <= STATE_CDATA_SECTION_END) {
  4134. reportError("eof-in-cdata", len, len, "error");
  4135. if (callbacks.comment !== undefined) {
  4136. // No `]]>` closed it, so everything after `<![CDATA[` is content.
  4137. pos = callbacks.comment(input, commentStart, len, commentDataStart, len);
  4138. }
  4139. } else if (state >= STATE_DOCTYPE && state <= STATE_BOGUS_DOCTYPE) {
  4140. // EOF in bogus DOCTYPE emits the token with no parse error (spec).
  4141. if (state !== STATE_BOGUS_DOCTYPE) {
  4142. reportError("eof-in-doctype", len, len, "error");
  4143. doctypeForceQuirks = true;
  4144. }
  4145. if (callbacks.doctype !== undefined) {
  4146. pos = callbacks.doctype(
  4147. input,
  4148. commentStart,
  4149. len,
  4150. doctypeNameStart,
  4151. doctypeNameEnd,
  4152. doctypePublicStart,
  4153. doctypePublicEnd,
  4154. doctypeSystemStart,
  4155. doctypeSystemEnd,
  4156. doctypeForceQuirks
  4157. );
  4158. }
  4159. } else {
  4160. if (
  4161. state === STATE_SCRIPT_DATA_ESCAPED ||
  4162. state === STATE_SCRIPT_DATA_ESCAPED_DASH ||
  4163. state === STATE_SCRIPT_DATA_ESCAPED_DASH_DASH ||
  4164. state === STATE_SCRIPT_DATA_ESCAPED_LESS_THAN_SIGN ||
  4165. state === STATE_SCRIPT_DATA_DOUBLE_ESCAPED ||
  4166. state === STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH ||
  4167. state === STATE_SCRIPT_DATA_DOUBLE_ESCAPED_DASH_DASH ||
  4168. state === STATE_SCRIPT_DATA_DOUBLE_ESCAPED_LESS_THAN_SIGN ||
  4169. state === STATE_SCRIPT_DATA_DOUBLE_ESCAPE_END
  4170. ) {
  4171. // Inside `<script><!-- … ` at EOF — spec calls this an
  4172. // eof-in-script-html-comment-like-text parse error. The
  4173. // less-than-sign and double-escape-end states reconsume back
  4174. // into the (double-)escaped state on EOF per spec, which then
  4175. // hits this same error.
  4176. reportError("eof-in-script-html-comment-like-text", len, len, "error");
  4177. } else if (state === STATE_TAG_OPEN || state === STATE_END_TAG_OPEN) {
  4178. // `<` or `</` with nothing after; spec calls this
  4179. // eof-before-tag-name. The lone `<` / `</` is preserved in the
  4180. // pending text span which is flushed below.
  4181. reportError("eof-before-tag-name", len, len, "warning");
  4182. }
  4183. if (textStart < len && callbacks.text !== undefined) {
  4184. callbacks.text(input, textStart, len);
  4185. }
  4186. }
  4187. return pos;
  4188. };
  4189. // WHATWG numeric-character-reference-end Windows-1252 remap table for the
  4190. // 0x80-0x9F range. Per spec these C1 control code points decode to the
  4191. // corresponding Windows-1252 glyph (with a parse error) rather than to the
  4192. // raw C1 control character.
  4193. const NUMERIC_C1_REMAP = {
  4194. 0x80: "€",
  4195. 0x82: "‚",
  4196. 0x83: "ƒ",
  4197. 0x84: "„",
  4198. 0x85: "…",
  4199. 0x86: "†",
  4200. 0x87: "‡",
  4201. 0x88: "ˆ",
  4202. 0x89: "‰",
  4203. 0x8a: "Š",
  4204. 0x8b: "‹",
  4205. 0x8c: "Œ",
  4206. 0x8e: "Ž",
  4207. 0x91: "‘",
  4208. 0x92: "’",
  4209. 0x93: "“",
  4210. 0x94: "”",
  4211. 0x95: "•",
  4212. 0x96: "–",
  4213. 0x97: "—",
  4214. 0x98: "˜",
  4215. 0x99: "™",
  4216. 0x9a: "š",
  4217. 0x9b: "›",
  4218. 0x9c: "œ",
  4219. 0x9e: "ž",
  4220. 0x9f: "Ÿ"
  4221. };
  4222. /**
  4223. * @param {number} code numeric character reference code point
  4224. * @returns {string} decoded character per WHATWG remap rules
  4225. */
  4226. const decodeNumericReference = (code) => {
  4227. // Per WHATWG numeric-character-reference-end-state:
  4228. // - 0x00, > 0x10FFFF, or surrogate (0xD800-0xDFFF) -> U+FFFD.
  4229. // - 0x80-0x9F -> Windows-1252 remap (above).
  4230. // - Anything else (including noncharacters and C0 controls) -> the
  4231. // code point itself; we don't surface the spec's parse-error
  4232. // classes here since decoding is happening after the scanner ran.
  4233. if (code === 0 || code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
  4234. return "�";
  4235. }
  4236. if (code >= 0x80 && code <= 0x9f) {
  4237. const remapped = /** @type {Record<number, string>} */ (NUMERIC_C1_REMAP)[
  4238. code
  4239. ];
  4240. if (remapped !== undefined) return remapped;
  4241. }
  4242. return String.fromCodePoint(code);
  4243. };
  4244. /**
  4245. * Decode a single matched character reference.
  4246. * @param {string} match the matched reference text
  4247. * @param {number} nextCharCode char code following the match in the source (NaN at the end)
  4248. * @param {boolean=} isAttribute true when the match came from an attribute value
  4249. * @returns {string} decoded text, or `match` itself when it stays literal
  4250. */
  4251. /**
  4252. * Decode a named character reference spanning `[start, end)` of `str` (the
  4253. * `&` at `start`, the optional `;` included). Slices the name once; WHATWG
  4254. * longest-prefix and consumed-as-part-of-an-attribute semantics as before.
  4255. * @param {string} str the raw string
  4256. * @param {number} start offset of the reference's `&`
  4257. * @param {number} end offset just past the reference (past the `;` if present)
  4258. * @param {number} nextCharCode char code following the reference (NaN at the end)
  4259. * @param {boolean=} isAttribute true when the reference came from an attribute value
  4260. * @returns {string | undefined} decoded text, or `undefined` when it stays literal
  4261. */
  4262. const decodeNamedReference = (str, start, end, nextCharCode, isAttribute) => {
  4263. // Mirrors the old `match.slice(1)`: the name keeps its trailing `;`.
  4264. const name = str.slice(start + 1, end);
  4265. const matchEndsWithSemi = name.charCodeAt(name.length - 1) === 0x3b;
  4266. // Attribute-context guard: if the entity match didn't end with `;`
  4267. // and the next character in the source is `=` or ASCII
  4268. // alphanumeric, the WHATWG spec says to flush the literal text
  4269. // rather than decode. The greedy scan already absorbed any
  4270. // trailing alphanumerics, so the only candidate "next char" here
  4271. // is `=` (or any non-alphanumeric).
  4272. if (isAttribute && !matchEndsWithSemi && nextCharCode === 0x3d /* = */) {
  4273. return undefined;
  4274. }
  4275. // Fast path: the scan usually captures exactly one entity (`&amp;`,
  4276. // `&lt;`, `&nbsp;`, ...), so the whole `name` is the match.
  4277. if (name.length <= MAX_ENTITY_NAME_LEN) {
  4278. const full = HTML_ENTITIES[name];
  4279. if (full !== undefined) return full;
  4280. }
  4281. // Cap the longest-prefix search at MAX_ENTITY_NAME_LEN so pathological
  4282. // inputs like `&` + thousands of alphanumerics stay linear-time.
  4283. // Anything past that cap can't possibly match and is appended
  4284. // verbatim as part of `name.slice(i)`. The full-length case was just
  4285. // handled above, so start one shorter when it's the cap.
  4286. const searchLen =
  4287. name.length > MAX_ENTITY_NAME_LEN ? MAX_ENTITY_NAME_LEN : name.length - 1;
  4288. for (let i = searchLen; i > 0; i--) {
  4289. const prefix = name.slice(0, i);
  4290. if (HTML_ENTITIES[prefix] !== undefined) {
  4291. // Attribute-context longest-prefix guard: if the matched
  4292. // prefix doesn't end with `;` and the leftover starts with
  4293. // an alphanumeric character, leave literal per WHATWG.
  4294. if (
  4295. isAttribute &&
  4296. i < name.length &&
  4297. prefix.charCodeAt(prefix.length - 1) !== 0x3b
  4298. ) {
  4299. return undefined;
  4300. }
  4301. return HTML_ENTITIES[prefix] + name.slice(i);
  4302. }
  4303. }
  4304. return undefined;
  4305. };
  4306. /** @typedef {{ text: string, map: number[] | undefined }} DecodedEntitiesWithMap */
  4307. /**
  4308. * Decode HTML character references in a string. Handles all numeric
  4309. * references (with WHATWG remap of 0x00, surrogates, out-of-range, and the
  4310. * C1 Windows-1252 table) and the full WHATWG named character references
  4311. * table. Unknown or malformed references are left as literal text.
  4312. *
  4313. * When `isAttribute` is `true`, applies the WHATWG
  4314. * "consumed-as-part-of-an-attribute" rule: a named reference without a
  4315. * trailing `;` whose next character is `=` or ASCII alphanumeric is left
  4316. * undecoded, so e.g. `&amp=foo` stays literal in an attribute value but
  4317. * decodes to `&=foo` in text.
  4318. *
  4319. * With `withMap` the result also carries a boundary map from the decoded
  4320. * string back to raw offsets, so spans computed on the decoded text (e.g.
  4321. * srcset candidate URLs) can be translated to source ranges. `map[i]` is the
  4322. * raw offset of decoded boundary `i` (`0..text.length`); boundaries inside a
  4323. * reference's decoded text map to the reference start. `map` is `undefined`
  4324. * when nothing was decoded (then `text === str`).
  4325. * @overload
  4326. * @param {string} str the raw string from the token slice
  4327. * @param {boolean=} isAttribute true if `str` came from an attribute value
  4328. * @returns {string} decoded string
  4329. */
  4330. /**
  4331. * @overload
  4332. * @param {string} str the raw string from the token slice
  4333. * @param {boolean | undefined} isAttribute true if `str` came from an attribute value
  4334. * @param {true} withMap also build the decoded-to-raw boundary map
  4335. * @returns {DecodedEntitiesWithMap} decoded text and offset map
  4336. */
  4337. /**
  4338. * @param {string} str the raw string from the token slice
  4339. * @param {boolean=} isAttribute true if `str` came from an attribute value
  4340. * @param {boolean=} withMap also build the decoded-to-raw boundary map
  4341. * @returns {string | DecodedEntitiesWithMap} decoded string (with map when `withMap`)
  4342. */
  4343. const decodeEntities = (str, isAttribute, withMap) => {
  4344. // Hand-rolled scan of one of three reference forms, each with an optional
  4345. // trailing `;` — `&#x<hex>+` / `&#<dec>+` / `&<alpha><alnum>*` (kept separate
  4346. // so `&#65b` doesn't eat the `b` as hex): entity-dense text pays no regex
  4347. // machinery or per-match object, and a string whose references all stay
  4348. // literal is returned unchanged without rebuilding. Map bookkeeping is
  4349. // gated on `withMap` at the rare per-decoded-reference points, so the
  4350. // dominant no-map mode pays nothing for the unified implementation.
  4351. let amp = str.indexOf("&");
  4352. if (amp === -1) return withMap ? { text: str, map: undefined } : str;
  4353. let out = "";
  4354. let last = 0;
  4355. /** @type {number[] | undefined} */
  4356. let map;
  4357. do {
  4358. // `end` stays 0 when the `&` doesn't start a well-formed reference; a
  4359. // numeric reference's code point accumulates during the digit scan
  4360. // (saturating above the code-point range — every overflow decodes the
  4361. // same replacement) so it needs no slice at all.
  4362. let end = 0;
  4363. let code = -1;
  4364. let cc = str.charCodeAt(amp + 1);
  4365. if (cc === 0x23 /* # */) {
  4366. cc = str.charCodeAt(amp + 2);
  4367. let p;
  4368. if (cc === 0x78 /* x */ || cc === 0x58 /* X */) {
  4369. code = 0;
  4370. p = amp + 3;
  4371. while (isAsciiHexDigit((cc = str.charCodeAt(p)))) {
  4372. code = code * 16 + (cc <= 0x39 ? cc - 0x30 : (cc | 0x20) - 0x57);
  4373. if (code > 0x10ffff) code = 0x110000;
  4374. p++;
  4375. }
  4376. if (p > amp + 3) end = p;
  4377. } else {
  4378. code = 0;
  4379. p = amp + 2;
  4380. while (isAsciiDigit((cc = str.charCodeAt(p)))) {
  4381. code = code * 10 + (cc - 0x30);
  4382. if (code > 0x10ffff) code = 0x110000;
  4383. p++;
  4384. }
  4385. if (p > amp + 2) end = p;
  4386. }
  4387. } else if (isAsciiAlpha(cc)) {
  4388. code = -1;
  4389. let p = amp + 2;
  4390. while (isAsciiAlphanumeric(str.charCodeAt(p))) p++;
  4391. end = p;
  4392. }
  4393. if (end !== 0) {
  4394. if (str.charCodeAt(end) === 0x3b /* ; */) end++;
  4395. // Numeric references always decode; named ones may stay literal.
  4396. const decoded =
  4397. code >= 0
  4398. ? decodeNumericReference(code)
  4399. : decodeNamedReference(
  4400. str,
  4401. amp,
  4402. end,
  4403. str.charCodeAt(end),
  4404. isAttribute
  4405. );
  4406. if (decoded !== undefined) {
  4407. if (withMap) map = _mapReference(map, last, amp, decoded.length);
  4408. out += str.slice(last, amp);
  4409. out += decoded;
  4410. last = end;
  4411. }
  4412. amp = str.indexOf("&", end);
  4413. } else {
  4414. amp = str.indexOf("&", amp + 1);
  4415. }
  4416. } while (amp !== -1);
  4417. if (last === 0) return withMap ? { text: str, map: undefined } : str;
  4418. const text = out + str.slice(last);
  4419. return withMap
  4420. ? _finishMap(text, /** @type {number[]} */ (map), last, str.length)
  4421. : text;
  4422. };
  4423. // Map-mode bookkeeping for `decodeEntities`, kept out of the scanner body so
  4424. // the dominant no-map mode stays small enough to inline.
  4425. /**
  4426. * @param {number[] | undefined} map boundary map so far
  4427. * @param {number} last raw offset the literal run starts at
  4428. * @param {number} amp raw offset of the decoded reference
  4429. * @param {number} decodedLength length of the decoded text
  4430. * @returns {number[]} the map, with the literal run's and reference's boundaries appended
  4431. */
  4432. const _mapReference = (map, last, amp, decodedLength) => {
  4433. if (map === undefined) map = [];
  4434. for (let r = last; r < amp; r++) map.push(r);
  4435. for (let i = 0; i < decodedLength; i++) map.push(amp);
  4436. return map;
  4437. };
  4438. /**
  4439. * @param {string} text decoded text
  4440. * @param {number[]} map boundary map so far
  4441. * @param {number} last raw offset of the trailing literal run
  4442. * @param {number} rawLength raw string length
  4443. * @returns {DecodedEntitiesWithMap} the finished result
  4444. */
  4445. const _finishMap = (text, map, last, rawLength) => {
  4446. for (let r = last; r < rawLength; r++) map.push(r);
  4447. map.push(rawLength);
  4448. return { text, map };
  4449. };
  4450. /**
  4451. * Escape a string per the WHATWG "escape a string" algorithm
  4452. * (https://html.spec.whatwg.org/multipage/parsing.html#escapingString) in
  4453. * attribute mode: `&`, U+00A0 and whichever quote delimits the value \u2014 the
  4454. * other one stands literal, which is what makes `alt='say "hi"'` legal. CR/LF
  4455. * are additionally encoded as numeric references so the result stays
  4456. * single-line (usable in a `data:` URI). Single pass, returns the input
  4457. * unchanged when nothing needs escaping.
  4458. * @param {string} s string to escape
  4459. * @param {number=} delimiter the quote the value is written in, `"` by default
  4460. * @param {boolean=} minimal leave an `&` no reference can start after it bare
  4461. * @returns {string} HTML attribute-safe string
  4462. */
  4463. const escapeAttribute = (s, delimiter, minimal) => {
  4464. const quote = delimiter === CC_APOSTROPHE ? CC_APOSTROPHE : CC_QUOTATION_MARK;
  4465. // fast path: native indexOf scans beat any per-character loop
  4466. if (
  4467. !s.includes("&") &&
  4468. !s.includes(quote === CC_APOSTROPHE ? "'" : '"') &&
  4469. !s.includes("\u00A0") &&
  4470. !s.includes("\n") &&
  4471. !s.includes("\r")
  4472. ) {
  4473. return s;
  4474. }
  4475. let out = "";
  4476. let last = 0;
  4477. for (let i = 0; i < s.length; i++) {
  4478. let rep;
  4479. switch (s.charCodeAt(i)) {
  4480. case CC_AMPERSAND: {
  4481. // §13.2.5.41: a `&` the parser cannot continue into a reference is
  4482. // data. The minifier writes it bare; generated markup escapes it.
  4483. const next = s.charCodeAt(i + 1);
  4484. if (
  4485. minimal === true &&
  4486. next !== CC_NUMBER_SIGN &&
  4487. !(
  4488. (next >= 0x30 && next <= 0x39) ||
  4489. (next >= 0x41 && next <= 0x5a) ||
  4490. (next >= 0x61 && next <= 0x7a)
  4491. )
  4492. ) {
  4493. continue;
  4494. }
  4495. rep = "&amp;";
  4496. break;
  4497. }
  4498. // Only the delimiter is escaped, so the compare costs a quote, not a
  4499. // character: everything ordinary still falls straight through.
  4500. case CC_QUOTATION_MARK:
  4501. if (quote !== CC_QUOTATION_MARK) continue;
  4502. rep = "&quot;";
  4503. break;
  4504. case CC_APOSTROPHE:
  4505. if (quote !== CC_APOSTROPHE) continue;
  4506. rep = "&#39;";
  4507. break;
  4508. case CC_NO_BREAK_SPACE:
  4509. rep = "&nbsp;";
  4510. break;
  4511. case CC_LF:
  4512. rep = "&#10;";
  4513. break;
  4514. case CC_CR:
  4515. rep = "&#13;";
  4516. break;
  4517. default:
  4518. continue;
  4519. }
  4520. out += s.slice(last, i) + rep;
  4521. last = i + 1;
  4522. }
  4523. return out + s.slice(last);
  4524. };
  4525. /**
  4526. * Escape a string per the WHATWG "escape a string" algorithm in text mode:
  4527. * `&`, U+00A0, `<` and `>`. CR/LF are additionally encoded as numeric
  4528. * references so the result stays single-line (usable in a `data:` URI).
  4529. * Single pass, returns the input unchanged when nothing needs escaping.
  4530. * @param {string} s string to escape
  4531. * @returns {string} HTML text-content-safe string
  4532. */
  4533. const escapeText = (s) => {
  4534. // fast path: native indexOf scans beat any per-character loop
  4535. if (
  4536. !s.includes("&") &&
  4537. !s.includes("<") &&
  4538. !s.includes(">") &&
  4539. !s.includes("\u00A0") &&
  4540. !s.includes("\n") &&
  4541. !s.includes("\r")
  4542. ) {
  4543. return s;
  4544. }
  4545. let out = "";
  4546. let last = 0;
  4547. for (let i = 0; i < s.length; i++) {
  4548. let rep;
  4549. switch (s.charCodeAt(i)) {
  4550. case CC_AMPERSAND:
  4551. rep = "&amp;";
  4552. break;
  4553. case CC_LESS_THAN:
  4554. rep = "&lt;";
  4555. break;
  4556. case CC_GREATER_THAN:
  4557. rep = "&gt;";
  4558. break;
  4559. case CC_NO_BREAK_SPACE:
  4560. rep = "&nbsp;";
  4561. break;
  4562. case CC_LF:
  4563. rep = "&#10;";
  4564. break;
  4565. case CC_CR:
  4566. rep = "&#13;";
  4567. break;
  4568. default:
  4569. continue;
  4570. }
  4571. out += s.slice(last, i) + rep;
  4572. last = i + 1;
  4573. }
  4574. return out + s.slice(last);
  4575. };
  4576. /**
  4577. * @param {string} name meta name
  4578. * @param {string} content meta content
  4579. * @returns {string} meta tag
  4580. */
  4581. const metaTag = (name, content) => {
  4582. // og: uses property=; all others (including twitter:) use name=
  4583. const attr = name.startsWith("og:")
  4584. ? `property="${escapeAttribute(name)}"`
  4585. : `name="${escapeAttribute(name)}"`;
  4586. return `<meta ${attr} content="${escapeAttribute(content)}">`;
  4587. };
  4588. /**
  4589. * @param {string | { href: string, target?: string }} base base option
  4590. * @returns {string} base tag
  4591. */
  4592. const baseTag = (base) => {
  4593. const href = typeof base === "string" ? base : base.href;
  4594. const targetAttr =
  4595. typeof base === "object" && base.target
  4596. ? ` target="${escapeAttribute(base.target)}"`
  4597. : "";
  4598. return `<base href="${escapeAttribute(href)}"${targetAttr}>`;
  4599. };
  4600. /**
  4601. * Serializes head tags in spec order — charset, base, meta, title — for a page
  4602. * built from scratch; an authored page merges the same options in `parse`.
  4603. * @param {OutputHtmlOptions} opts html options
  4604. * @returns {string} head tags string
  4605. */
  4606. const buildHeadTags = (opts) => {
  4607. let out = "";
  4608. const meta = opts.meta;
  4609. if (meta && meta.charset) {
  4610. out += `<meta charset="${escapeAttribute(meta.charset)}">`;
  4611. }
  4612. if (opts.base) out += baseTag(opts.base);
  4613. if (meta) {
  4614. for (const [name, content] of Object.entries(meta)) {
  4615. if (name === "charset") continue;
  4616. out += metaTag(name, content);
  4617. }
  4618. }
  4619. if (opts.title) {
  4620. out += `<title>${escapeText(opts.title)}</title>`;
  4621. }
  4622. return out;
  4623. };
  4624. // cspell:ignore definitionurl malignmark mglyph selectedcontent megamorphic attributeless rowspan imagesizes novalidate maxlength
  4625. // WHATWG HTML tree construction (https://html.spec.whatwg.org/multipage/parsing.html#tree-construction)
  4626. // on top of tokenize. Scripting is always disabled (webpack is a build tool).
  4627. // Namespaces (mirrors swc_html_ast::Namespace)
  4628. const NS_HTML = 0;
  4629. const NS_MATHML = 1;
  4630. const NS_SVG = 2;
  4631. /**
  4632. * AST node `type` discriminators. Numeric for the same reason as the CSS
  4633. * `NodeType`: compact integer `===` dispatch on the tree-construction and
  4634. * visitor-walk hot paths.
  4635. * @type {{ Document: 1, DocumentFragment: 2, Element: 3, Text: 4, Comment: 5, Doctype: 6, ProcessingInstruction: 7 }}
  4636. */
  4637. const NodeType = {
  4638. Document: 1,
  4639. DocumentFragment: 2,
  4640. Element: 3,
  4641. Text: 4,
  4642. Comment: 5,
  4643. Doctype: 6,
  4644. ProcessingInstruction: 7
  4645. };
  4646. /**
  4647. * A contiguous run of attribute ids in the attribute columns — how a start-tag
  4648. * token and an element refer to their attributes. `start` is the first id
  4649. * (`count` 0 = none).
  4650. * @typedef {{ start: number, count: number }} AttributeRun
  4651. */
  4652. // Shared frozen empty run for attributeless elements and synthesized tags.
  4653. const EMPTY_ATTRS = /** @type {AttributeRun} */ (
  4654. Object.freeze({ start: 0, count: 0 })
  4655. );
  4656. // === Struct-of-arrays AST backend ===
  4657. // One AST node = one integer id (`HtmlNodeRef`) indexing the parallel columns
  4658. // below — no per-node object and no per-parent children array. Tree shape
  4659. // lives in the four link columns (parent / firstChild / lastChild /
  4660. // nextSibling); the only heap references are the string payload / attribute
  4661. // name-and-value side arrays. Columns are module-level and reused
  4662. // across parses (grown, shrunk on release only past `_COLUMN_SHRINK_CAPACITY`,
  4663. // mirroring the CSS parser's column backend), so a steady-state parse
  4664. // allocates almost nothing per node; consumers must fully read a tree before
  4665. // the next `parseHtml` call. Id 0 is reserved as "no node" so the link columns
  4666. // can use 0 as null.
  4667. let _nodeCapacity = 0;
  4668. let _nodeCount = 0;
  4669. // Highest id `_nodeCount` ever reached this parse. Streaming rewinds the
  4670. // allocator, so `_nodeCount` alone no longer bounds the ids that were written —
  4671. // the release pass has to clear up to here or the recycled slots keep their
  4672. // strings alive until a later parse happens to overwrite them.
  4673. let _nodeHighWater = 0;
  4674. /** `NodeType` per node */
  4675. let _nodeTypes = new Uint8Array(0);
  4676. /** bits 0-1 namespace (`NS_*`), bit 2 self-closing (void), bit 3 template content */
  4677. let _nodeFlags = new Uint8Array(0);
  4678. let _nodeStarts = new Int32Array(0);
  4679. let _nodeEnds = new Int32Array(0);
  4680. /** end offset of an element's opening tag (after `>`) */
  4681. let _nodeTagEnds = new Int32Array(0);
  4682. /** end offset of an element's tag name */
  4683. let _nodeNameEnds = new Int32Array(0);
  4684. /**
  4685. * under `skip.text`, a raw-text element's body-end offset (`HtmlAstSkip`); for
  4686. * a `<template>` (`FLAG_HAS_TEMPLATE`) it instead holds the content-fragment ref
  4687. */
  4688. let _nodeContentEnds = new Int32Array(0);
  4689. let _nodeParents = new Int32Array(0);
  4690. let _nodeFirstChildren = new Int32Array(0);
  4691. let _nodeLastChildren = new Int32Array(0);
  4692. let _nodeNextSiblings = new Int32Array(0);
  4693. /** @type {string[]} tag name / text data / comment data / doctype name */
  4694. const _nodeStrings = [];
  4695. /** first attribute id of an element's contiguous run */
  4696. let _nodeAttrStarts = new Int32Array(0);
  4697. /** attribute count of an element's run */
  4698. let _nodeAttrCounts = new Int32Array(0);
  4699. // The single doctype node's public/system ids (a document inserts at most one
  4700. // doctype node — later doctype tokens are ignored — so no column is needed).
  4701. /** @type {string | null} */
  4702. let _doctypePublicId = null;
  4703. /** @type {string | null} */
  4704. let _doctypeSystemId = null;
  4705. const NS_MASK = 3;
  4706. const FLAG_SELF_CLOSING = 4;
  4707. // A `<template>` element: `_nodeContentEnds` holds its content-fragment ref
  4708. // instead of a raw-text body-end offset (the two never coexist on one node).
  4709. const FLAG_HAS_TEMPLATE = 8;
  4710. // A `<template shadowrootmode>` kept where it was written, and its host: the
  4711. // engine takes it out of the tree, so nothing may move it away from that host.
  4712. const FLAG_SHADOW_ROOT = 16;
  4713. const FLAG_SHADOW_HOST = 32;
  4714. // The element whose content prints from source, and everything still open around
  4715. // it. A flag, not a lookup: the walk asks this of every node.
  4716. const FLAG_FOSTER_REGION = 128;
  4717. /**
  4718. * @param {HtmlNodeRef} el an element
  4719. * @returns {boolean} whether a shadow root can be attached to it
  4720. */
  4721. const _isShadowHost = (el) => {
  4722. if (_namespaceOf(el) !== NS_HTML) return false;
  4723. const tagName = _tagNameOf(el);
  4724. if (SHADOW_HOSTS.has(tagName)) return true;
  4725. return (
  4726. tagName.includes("-") &&
  4727. tagName.charCodeAt(0) >= 97 &&
  4728. tagName.charCodeAt(0) <= 122 &&
  4729. !RESERVED_ELEMENT_NAMES.has(tagName)
  4730. );
  4731. };
  4732. /** @type {(el: HtmlNodeRef) => string} */
  4733. const _tagNameOf = (el) => _nodeStrings[el];
  4734. // A `<template>`'s content-fragment ref (in the shared `_nodeContentEnds` slot),
  4735. // or 0 when `el` is not a template — the tree builder's spec "template contents".
  4736. /** @type {(el: HtmlNodeRef) => HtmlDocumentFragment} */
  4737. const _templateContentOf = (el) =>
  4738. (_nodeFlags[el] & FLAG_HAS_TEMPLATE) !== 0 ? _nodeContentEnds[el] : 0;
  4739. /** @type {(el: HtmlNodeRef) => number} */
  4740. const _namespaceOf = (el) => _nodeFlags[el] & NS_MASK;
  4741. // === Attribute columns ===
  4742. // One attribute = one integer id into these columns; an element (and a
  4743. // start-tag token) holds a contiguous run. The value string is derived from
  4744. // the source by offset on read — `_attrValues` carries an override only for
  4745. // valueless attributes (`""`) and offset-less adoption-agency clones — and the
  4746. // html5lib serializer name is derived from the adjusted name plus one flag
  4747. // bit, so per attribute only the interned name pointer is retained.
  4748. let _attrCapacity = 0;
  4749. let _attrCount = 0;
  4750. // Elements a repeated `<html>` / `<body>` start tag merged attributes onto
  4751. // (§13.2.6.4.4, §13.2.6.4.7). The merged names are not in the element's source
  4752. // open tag, so the printer rebuilds that tag instead of echoing it.
  4753. const _mergedAttrNodes = new Set();
  4754. // A processing instruction's target; its data rides in `_nodeStrings` like a
  4755. // comment's. A map, since a second string column per node would cost more.
  4756. /** @type {Map<HtmlNodeRef, string>} */
  4757. const _piTargets = new Map();
  4758. let _attrNameStarts = new Int32Array(0);
  4759. let _attrNameEnds = new Int32Array(0);
  4760. let _attrValueStarts = new Int32Array(0);
  4761. let _attrValueEnds = new Int32Array(0);
  4762. /** bit 0: name has a `FOREIGN_ATTR_NS` serializer name (set on foreign adjust) */
  4763. let _attrFlags = new Uint8Array(0);
  4764. /** @type {string[]} lowercased (foreign-content: adjusted) attribute name */
  4765. const _attrNames = [];
  4766. /** @type {(string | null)[]} value override (null = slice the source by offset) */
  4767. const _attrValues = [];
  4768. /** source of the current parse, for by-offset attribute values */
  4769. let _htmlSource = "";
  4770. /**
  4771. * @param {number} need minimum capacity
  4772. * @param {boolean=} exact allocate `need` directly (initial pre-size) instead of doubling
  4773. */
  4774. const _growAttrColumns = (need, exact) => {
  4775. let cap = _attrCapacity || 4096;
  4776. if (exact) {
  4777. if (need > cap) cap = need;
  4778. } else {
  4779. while (cap < need) cap *= 2;
  4780. // A capacity over the shrink threshold is released after the parse, so
  4781. // doubling just past the pre-size costs the next one its columns.
  4782. if (cap > _COLUMN_SHRINK_CAPACITY && need <= _COLUMN_SHRINK_CAPACITY) {
  4783. cap = _COLUMN_SHRINK_CAPACITY;
  4784. }
  4785. }
  4786. const nameStart = new Int32Array(cap);
  4787. nameStart.set(_attrNameStarts);
  4788. _attrNameStarts = nameStart;
  4789. const nameEnd = new Int32Array(cap);
  4790. nameEnd.set(_attrNameEnds);
  4791. _attrNameEnds = nameEnd;
  4792. const valStart = new Int32Array(cap);
  4793. valStart.set(_attrValueStarts);
  4794. _attrValueStarts = valStart;
  4795. const valEnd = new Int32Array(cap);
  4796. valEnd.set(_attrValueEnds);
  4797. _attrValueEnds = valEnd;
  4798. const fl = new Uint8Array(cap);
  4799. fl.set(_attrFlags);
  4800. _attrFlags = fl;
  4801. // Keep the string columns filled to capacity (see `_growNodeColumns`).
  4802. for (let i = _attrNames.length; i < cap; i++) {
  4803. _attrNames.push("");
  4804. _attrValues.push(null);
  4805. }
  4806. _attrCapacity = cap;
  4807. };
  4808. /** @type {(name: string, value: string | null, nameStart: number, nameEnd: number, valueStart: number, valueEnd: number) => number} */
  4809. const _allocAttr = (name, value, nameStart, nameEnd, valueStart, valueEnd) => {
  4810. const i = ++_attrCount;
  4811. if (i >= _attrCapacity) _growAttrColumns(i + 1);
  4812. _attrNameStarts[i] = nameStart;
  4813. _attrNameEnds[i] = nameEnd;
  4814. _attrValueStarts[i] = valueStart;
  4815. _attrValueEnds[i] = valueEnd;
  4816. _attrFlags[i] = 0;
  4817. // Ids are sequential, so these indexed writes append (arrays stay packed).
  4818. _attrNames[i] = name;
  4819. _attrValues[i] = value;
  4820. return i;
  4821. };
  4822. /** @type {(i: number) => string} */
  4823. const _attrValueOf = (i) => {
  4824. const v = _attrValues[i];
  4825. return v !== null
  4826. ? v
  4827. : _htmlSource.slice(_attrValueStarts[i], _attrValueEnds[i]);
  4828. };
  4829. // Linear name lookup in a run — attribute lists are short, a loop beats a Map.
  4830. /** @type {(start: number, count: number, name: string) => number} */
  4831. const _findAttr = (start, count, name) => {
  4832. for (let i = start; i < start + count; i++) {
  4833. if (_attrNames[i] === name) return i;
  4834. }
  4835. return 0;
  4836. };
  4837. /** @type {(i: number) => number} exact copy of an attribute into a new id */
  4838. const _copyAttr = (i) => {
  4839. const c = _allocAttr(
  4840. _attrNames[i],
  4841. _attrValues[i],
  4842. _attrNameStarts[i],
  4843. _attrNameEnds[i],
  4844. _attrValueStarts[i],
  4845. _attrValueEnds[i]
  4846. );
  4847. _attrFlags[c] = _attrFlags[i];
  4848. return c;
  4849. };
  4850. // html5lib serializer name, derived: a `FOREIGN_ATTR_NS`-adjusted attribute is
  4851. // flagged (its name is the table key), and a camelCase-adjusted name contains
  4852. // an uppercase letter (unadjusted names are always lowercased), serializing as
  4853. // itself. Everything else serializes as the plain name (undefined here).
  4854. /** @type {(i: number) => string | undefined} */
  4855. const _attrSerializedName = (i) => {
  4856. if ((_attrFlags[i] & 1) !== 0) return FOREIGN_ATTR_NS[_attrNames[i]];
  4857. const name = _attrNames[i];
  4858. return /[A-Z]/.test(name) ? name : undefined;
  4859. };
  4860. /**
  4861. * @param {number} need minimum capacity
  4862. * @param {boolean=} exact allocate `need` directly (initial pre-size) instead of doubling
  4863. */
  4864. const _growNodeColumns = (need, exact) => {
  4865. let cap = _nodeCapacity || 4096;
  4866. // `exact` (the initial pre-size): allocate the estimate directly. Doubling
  4867. // from 4096 would overshoot it to the next power of two (~2x) and waste that
  4868. // capacity for the process lifetime. Incremental growth still doubles.
  4869. if (exact) {
  4870. if (need > cap) cap = need;
  4871. } else {
  4872. while (cap < need) cap *= 2;
  4873. // A capacity over the shrink threshold is released after the parse, so
  4874. // doubling just past the pre-size costs the next one its columns.
  4875. if (cap > _COLUMN_SHRINK_CAPACITY && need <= _COLUMN_SHRINK_CAPACITY) {
  4876. cap = _COLUMN_SHRINK_CAPACITY;
  4877. }
  4878. }
  4879. const ty = new Uint8Array(cap);
  4880. ty.set(_nodeTypes);
  4881. _nodeTypes = ty;
  4882. const fl = new Uint8Array(cap);
  4883. fl.set(_nodeFlags);
  4884. _nodeFlags = fl;
  4885. const st = new Int32Array(cap);
  4886. st.set(_nodeStarts);
  4887. _nodeStarts = st;
  4888. const en = new Int32Array(cap);
  4889. en.set(_nodeEnds);
  4890. _nodeEnds = en;
  4891. const tagEnd = new Int32Array(cap);
  4892. tagEnd.set(_nodeTagEnds);
  4893. _nodeTagEnds = tagEnd;
  4894. const nameEnd = new Int32Array(cap);
  4895. nameEnd.set(_nodeNameEnds);
  4896. _nodeNameEnds = nameEnd;
  4897. const cEnd = new Int32Array(cap);
  4898. cEnd.set(_nodeContentEnds);
  4899. _nodeContentEnds = cEnd;
  4900. const parent = new Int32Array(cap);
  4901. parent.set(_nodeParents);
  4902. _nodeParents = parent;
  4903. const first = new Int32Array(cap);
  4904. first.set(_nodeFirstChildren);
  4905. _nodeFirstChildren = first;
  4906. const last = new Int32Array(cap);
  4907. last.set(_nodeLastChildren);
  4908. _nodeLastChildren = last;
  4909. const next = new Int32Array(cap);
  4910. next.set(_nodeNextSiblings);
  4911. _nodeNextSiblings = next;
  4912. const aStart = new Int32Array(cap);
  4913. aStart.set(_nodeAttrStarts);
  4914. _nodeAttrStarts = aStart;
  4915. const aCount = new Int32Array(cap);
  4916. aCount.set(_nodeAttrCounts);
  4917. _nodeAttrCounts = aCount;
  4918. // Keep the string column filled to capacity so per-node writes are in-place
  4919. // packed stores, not length-growing appends re-paying the regrowth cascade.
  4920. for (let i = _nodeStrings.length; i < cap; i++) _nodeStrings.push("");
  4921. _nodeCapacity = cap;
  4922. };
  4923. /** Start a new parse: invalidate all prior refs, release prior heap refs. */
  4924. const _resetAstColumns = () => {
  4925. // Overwrite the used prefix (releases the prior parse's strings) instead of
  4926. // truncating: a `length = 0` would right-size the backing store and make
  4927. // every write of the next parse a length-growing append again.
  4928. const written = _nodeCount > _nodeHighWater ? _nodeCount : _nodeHighWater;
  4929. const usedNodes = Math.min(written, _nodeStrings.length - 1);
  4930. for (let i = 0; i <= usedNodes; i++) _nodeStrings[i] = "";
  4931. const usedAttrs = Math.min(_attrCount, _attrNames.length - 1);
  4932. for (let i = 0; i <= usedAttrs; i++) {
  4933. _attrNames[i] = "";
  4934. _attrValues[i] = null;
  4935. }
  4936. _nodeCount = 0;
  4937. _nodeHighWater = 0;
  4938. _attrCount = 0;
  4939. _mergedAttrNodes.clear();
  4940. _piTargets.clear();
  4941. _doctypePublicId = null;
  4942. _doctypeSystemId = null;
  4943. };
  4944. // The typed-array columns grow to the largest document ever parsed; above this
  4945. // capacity they are re-shrunk on release so one pathological file can't pin
  4946. // tens of MB at module level for the process lifetime (~46 bytes/node,
  4947. // ~17 bytes/attribute of capacity across the columns).
  4948. const _COLUMN_SHRINK_CAPACITY = 65536;
  4949. // Release the side arrays' heap references (strings, attribute names/values)
  4950. // once a walk has consumed the tree, so the retained columns don't pin the
  4951. // parsed source until the next parse.
  4952. const _releaseAstColumns = () => {
  4953. // Kept regime: overwrite the used prefix in place (see `_resetAstColumns`);
  4954. // oversized arrays are dropped entirely, mirroring the typed columns below.
  4955. if (_nodeStrings.length > _COLUMN_SHRINK_CAPACITY) {
  4956. _nodeStrings.length = 0;
  4957. } else {
  4958. const written = _nodeCount > _nodeHighWater ? _nodeCount : _nodeHighWater;
  4959. const usedNodes = Math.min(written, _nodeStrings.length - 1);
  4960. for (let i = 0; i <= usedNodes; i++) _nodeStrings[i] = "";
  4961. }
  4962. if (_attrNames.length > _COLUMN_SHRINK_CAPACITY) {
  4963. _attrNames.length = 0;
  4964. _attrValues.length = 0;
  4965. } else {
  4966. const usedAttrs = Math.min(_attrCount, _attrNames.length - 1);
  4967. for (let i = 0; i <= usedAttrs; i++) {
  4968. _attrNames[i] = "";
  4969. _attrValues[i] = null;
  4970. }
  4971. }
  4972. // The printer's sort buffers grew to the widest tag and longest token list
  4973. // this document held; dropping them keeps one page off the module forever.
  4974. _sortOrderScratch.length = 0;
  4975. _sortKeyScratch.length = 0;
  4976. _tokenSpanStarts.length = 0;
  4977. _tokenSpanEnds.length = 0;
  4978. _nodeCount = 0;
  4979. _nodeHighWater = 0;
  4980. _attrCount = 0;
  4981. _mergedAttrNodes.clear();
  4982. _piTargets.clear();
  4983. _htmlSource = "";
  4984. _doctypePublicId = null;
  4985. _doctypeSystemId = null;
  4986. if (_nodeCapacity > _COLUMN_SHRINK_CAPACITY) {
  4987. _nodeCapacity = 0;
  4988. _nodeTypes = new Uint8Array(0);
  4989. _nodeFlags = new Uint8Array(0);
  4990. _nodeStarts = new Int32Array(0);
  4991. _nodeEnds = new Int32Array(0);
  4992. _nodeTagEnds = new Int32Array(0);
  4993. _nodeNameEnds = new Int32Array(0);
  4994. _nodeContentEnds = new Int32Array(0);
  4995. _nodeParents = new Int32Array(0);
  4996. _nodeFirstChildren = new Int32Array(0);
  4997. _nodeLastChildren = new Int32Array(0);
  4998. _nodeNextSiblings = new Int32Array(0);
  4999. _nodeAttrStarts = new Int32Array(0);
  5000. _nodeAttrCounts = new Int32Array(0);
  5001. }
  5002. if (_attrCapacity > _COLUMN_SHRINK_CAPACITY) {
  5003. _attrCapacity = 0;
  5004. _attrNameStarts = new Int32Array(0);
  5005. _attrNameEnds = new Int32Array(0);
  5006. _attrValueStarts = new Int32Array(0);
  5007. _attrValueEnds = new Int32Array(0);
  5008. _attrFlags = new Uint8Array(0);
  5009. }
  5010. };
  5011. /** @type {(type: number, start: number, end: number) => HtmlNodeRef} */
  5012. // The element-only columns (tagEnds/nameEnds/contentEnds/attrStarts/
  5013. // attrCounts) are left stale here — they are only ever read for Element
  5014. // nodes, and every Element writes them in mkEl/cloneSubtree.
  5015. const _allocNode = (type, start, end) => {
  5016. const i = ++_nodeCount;
  5017. if (i >= _nodeCapacity) _growNodeColumns(i + 1);
  5018. _nodeTypes[i] = type;
  5019. _nodeFlags[i] = 0;
  5020. _nodeStarts[i] = start;
  5021. _nodeEnds[i] = end;
  5022. _nodeParents[i] = 0;
  5023. _nodeFirstChildren[i] = 0;
  5024. _nodeLastChildren[i] = 0;
  5025. _nodeNextSiblings[i] = 0;
  5026. // `_nodeStrings[i]` is written by every caller right after this returns
  5027. // (tag name / text / comment data; "" at the few string-less sites), as a
  5028. // packed in-place store — no placeholder double-write here.
  5029. return i;
  5030. };
  5031. // Raw child append — no text merging, no `<template>` content redirect (the
  5032. // tree builder layers those on top). `node` must be detached (`next` = 0).
  5033. /** @type {(parent: HtmlNodeRef, node: HtmlNodeRef) => void} */
  5034. const _appendChild = (parent, node) => {
  5035. _nodeParents[node] = parent;
  5036. const last = _nodeLastChildren[parent];
  5037. if (last === 0) _nodeFirstChildren[parent] = node;
  5038. else _nodeNextSiblings[last] = node;
  5039. _nodeLastChildren[parent] = node;
  5040. };
  5041. /** @type {(data: string, start: number, end: number) => HtmlNodeRef} */
  5042. const _makeTextNode = (data, start, end) => {
  5043. const i = _allocNode(NodeType.Text, start, end);
  5044. _nodeStrings[i] = data;
  5045. return i;
  5046. };
  5047. /** @type {(data: string, start: number, end: number) => HtmlNodeRef} */
  5048. const _makeCommentNode = (data, start, end) => {
  5049. const i = _allocNode(NodeType.Comment, start, end);
  5050. _nodeStrings[i] = data;
  5051. return i;
  5052. };
  5053. /** @type {(target: string, data: string, start: number, end: number) => HtmlNodeRef} */
  5054. const _makeProcessingInstructionNode = (target, data, start, end) => {
  5055. const i = _allocNode(NodeType.ProcessingInstruction, start, end);
  5056. _nodeStrings[i] = data;
  5057. _piTargets.set(i, target);
  5058. return i;
  5059. };
  5060. // Marker entry in the active-formatting-elements list (never a valid ref).
  5061. const AFE_MARKER = -1;
  5062. // Clone of an element's attribute run: keep name/value (and the serializer
  5063. // name flag) but drop source offsets so the consumer doesn't emit a duplicate
  5064. // dependency for the reopened element's spans. Values are materialized since
  5065. // the offsets are gone.
  5066. const cloneAttrs = (/** @type {HtmlElement} */ el) => {
  5067. const start = _nodeAttrStarts[el];
  5068. const count = _nodeAttrCounts[el];
  5069. const newStart = _attrCount + 1;
  5070. for (let i = start; i < start + count; i++) {
  5071. const c = _allocAttr(_attrNames[i], _attrValueOf(i), -1, -1, -1, -1);
  5072. _attrFlags[c] = _attrFlags[i];
  5073. }
  5074. return { start: newStart, count };
  5075. };
  5076. // Merge a repeated `<html>`/`<body>` tag's attributes into the element: only
  5077. // names not already present are added (in source order after the existing
  5078. // ones). Runs are contiguous, so any addition re-allocates the whole run; the
  5079. // old slots are orphaned (at most once per repeated tag, rare).
  5080. const mergeAttrs = (
  5081. /** @type {HtmlElement} */ el,
  5082. /** @type {AttributeRun} */ run
  5083. ) => {
  5084. const start = _nodeAttrStarts[el];
  5085. const count = _nodeAttrCounts[el];
  5086. let extra = 0;
  5087. for (let i = run.start; i < run.start + run.count; i++) {
  5088. if (_findAttr(start, count, _attrNames[i]) === 0) extra++;
  5089. }
  5090. if (extra === 0) return;
  5091. const newStart = _attrCount + 1;
  5092. for (let i = start; i < start + count; i++) _copyAttr(i);
  5093. for (let i = run.start; i < run.start + run.count; i++) {
  5094. if (_findAttr(start, count, _attrNames[i]) === 0) _copyAttr(i);
  5095. }
  5096. _nodeAttrStarts[el] = newStart;
  5097. _nodeAttrCounts[el] = count + extra;
  5098. _mergedAttrNodes.add(el);
  5099. };
  5100. /**
  5101. * A materialized attribute as returned by `A.attributes` (tests/tooling) —
  5102. * the parser-facing representation is an id into the attribute columns, read
  5103. * through the scalar `A.attr*` accessors.
  5104. * @typedef {object} HtmlAttribute
  5105. * @property {string} name lowercased (and, in foreign content, adjusted) attribute name
  5106. * @property {string} value
  5107. * @property {string=} serializedName name used by the html5lib tree serializer (foreign-namespaced)
  5108. * @property {number} nameStart source offset, or -1 on adoption-agency clones
  5109. * @property {number} nameEnd
  5110. * @property {number} valueStart source offset, or -1 when valueless / on clones
  5111. * @property {number} valueEnd
  5112. */
  5113. /**
  5114. * A node reference into the struct-of-arrays AST: an integer id indexing the
  5115. * parallel `_h*` columns. Read fields through the exported accessor `A`. Refs
  5116. * are only valid until the next `parseHtml` call — the columns are reused
  5117. * across parses — so consume a tree fully before parsing again.
  5118. * @typedef {number} HtmlNodeRef
  5119. */
  5120. /** @typedef {HtmlNodeRef} HtmlElement ref to an Element node */
  5121. /** @typedef {HtmlNodeRef} HtmlText ref to a Text node */
  5122. /** @typedef {HtmlNodeRef} HtmlComment ref to a Comment node */
  5123. /** @typedef {HtmlNodeRef} HtmlDoctype ref to a Doctype node */
  5124. /** @typedef {HtmlNodeRef} HtmlProcessingInstruction ref to a ProcessingInstruction node */
  5125. /** @typedef {HtmlNodeRef} HtmlDocument ref to the Document node */
  5126. /** @typedef {HtmlNodeRef} HtmlDocumentFragment ref to a DocumentFragment node */
  5127. /** @typedef {HtmlNodeRef} HtmlNode */
  5128. /**
  5129. * An attribute reference: an integer id into the attribute columns, read
  5130. * through the `A.attr*` accessors. Same validity contract as `HtmlNodeRef`.
  5131. * @typedef {number} HtmlAttributeRef
  5132. */
  5133. /** @typedef {{ start: number, end: number, tagEnd: number, nameEnd: number }} TagPos */
  5134. // Tree-construction token `type` discriminators. Numeric for the same reason
  5135. // as `NodeType` / the CSS `TT_*` constants: the insertion modes dispatch on
  5136. // `t.type` per token, and integer `===` beats string comparison there.
  5137. const TOKEN_CHAR = 1;
  5138. const TOKEN_COMMENT = 2;
  5139. const TOKEN_DOCTYPE = 3;
  5140. const TOKEN_START_TAG = 4;
  5141. const TOKEN_END_TAG = 5;
  5142. const TOKEN_EOF = 6;
  5143. const TOKEN_PROCESSING_INSTRUCTION = 7;
  5144. /** @typedef {{ type: typeof TOKEN_CHAR, data: string, start: number, end: number }} CharToken */
  5145. /** @typedef {{ type: typeof TOKEN_COMMENT, data: string, start: number, end: number }} CommentToken */
  5146. /** @typedef {{ type: typeof TOKEN_DOCTYPE, name: string, publicId: (string | null), systemId: (string | null), forceQuirks: boolean, start: number, end: number }} DoctypeToken */
  5147. /** @typedef {{ type: typeof TOKEN_START_TAG, name: string, attrs: AttributeRun, selfClosing: boolean, pos: TagPos, swallowNewline?: boolean }} StartTagToken */
  5148. /** @typedef {{ type: typeof TOKEN_END_TAG, name: string, pos: TagPos }} EndTagToken */
  5149. /** @typedef {{ type: typeof TOKEN_EOF }} EofToken */
  5150. /** @typedef {{ type: typeof TOKEN_PROCESSING_INSTRUCTION, name: string, data: string, start: number, end: number }} ProcessingInstructionToken the target rides in `name`, as the doctype's does */
  5151. // Every insertion mode handles a processing instruction token on the same arc
  5152. // as a comment token, so the arcs test both.
  5153. const isCommentOrProcessingInstruction = (/** @type {Token} */ t) =>
  5154. t.type === TOKEN_COMMENT || t.type === TOKEN_PROCESSING_INSTRUCTION;
  5155. /**
  5156. * Internal token passed through the tree-construction insertion modes.
  5157. * @typedef {CharToken | CommentToken | DoctypeToken | StartTagToken | EndTagToken | EofToken | ProcessingInstructionToken} Token
  5158. */
  5159. /**
  5160. * The tree builder reuses a single mutable token (with a reused `pos`) instead
  5161. * of allocating one object per tokenizer callback. All fields are always
  5162. * present so the shape never changes — keeping the `process`/insertion-mode
  5163. * `t.*` reads monomorphic — and fields irrelevant to the current `type` carry
  5164. * stale values that those handlers never read. Tokens that must outlive the
  5165. * current callback (buffered table characters, synthesized re-dispatches) are
  5166. * copied into fresh plain objects instead.
  5167. * @typedef {{ type: number, name: string, data: string, attrs: AttributeRun, selfClosing: boolean, start: number, end: number, publicId: (string | null), systemId: (string | null), forceQuirks: boolean, swallowNewline: boolean, pos: TagPos }} MutableToken
  5168. */
  5169. /** @typedef {{ parent: HtmlNodeRef, beforeNode: HtmlNodeRef }} InsertionPlace `beforeNode` 0 = plain append */
  5170. // Insertion modes (§13.2.4.1). Numeric for the same reason as the token and
  5171. // `NodeType` enums: `runMode` dispatches on `mode` once per token.
  5172. const MODE_INITIAL = 1;
  5173. const MODE_BEFORE_HTML = 2;
  5174. const MODE_BEFORE_HEAD = 3;
  5175. const MODE_IN_HEAD = 4;
  5176. const MODE_IN_HEAD_NOSCRIPT = 5;
  5177. const MODE_AFTER_HEAD = 6;
  5178. const MODE_IN_BODY = 7;
  5179. const MODE_TEXT = 8;
  5180. const MODE_IN_TABLE = 9;
  5181. const MODE_IN_TABLE_TEXT = 10;
  5182. const MODE_IN_CAPTION = 11;
  5183. const MODE_IN_COLUMN_GROUP = 12;
  5184. const MODE_IN_TABLE_BODY = 13;
  5185. const MODE_IN_ROW = 14;
  5186. const MODE_IN_CELL = 15;
  5187. const MODE_IN_TEMPLATE = 16;
  5188. const MODE_AFTER_BODY = 17;
  5189. const MODE_IN_FRAMESET = 18;
  5190. const MODE_AFTER_FRAMESET = 19;
  5191. const MODE_AFTER_AFTER_BODY = 20;
  5192. const MODE_AFTER_AFTER_FRAMESET = 21;
  5193. // The §13.2.6.4.18 re-dispatch table, bound to this file's numbering. A name the
  5194. // binding does not cover would silently dispatch to `undefined`, so it throws.
  5195. /** @type {Record<string, number>} */
  5196. const MODE_BY_NAME = {
  5197. IN_TABLE: MODE_IN_TABLE,
  5198. IN_COLUMN_GROUP: MODE_IN_COLUMN_GROUP,
  5199. IN_TABLE_BODY: MODE_IN_TABLE_BODY,
  5200. IN_ROW: MODE_IN_ROW
  5201. };
  5202. const TEMPLATE_START_TAG_MODES = new Map(
  5203. Object.entries(TEMPLATE_START_TAG_MODE_NAMES).map(([tag, name]) => {
  5204. const mode = MODE_BY_NAME[name];
  5205. if (mode === undefined) throw new Error(`Unknown insertion mode ${name}`);
  5206. return [tag, mode];
  5207. })
  5208. );
  5209. // MathML/SVG specials handled via namespace checks below.
  5210. const isSpecial = (/** @type {HtmlElement} */ el) => {
  5211. const ns = _namespaceOf(el);
  5212. const tag = _tagNameOf(el);
  5213. if (ns === NS_HTML) return SPECIAL.has(tag);
  5214. if (ns === NS_MATHML) return MATHML_SPECIAL.has(tag);
  5215. if (ns === NS_SVG) return SVG_SPECIAL.has(tag.toLowerCase());
  5216. return false;
  5217. };
  5218. // `<font color|face|size>` breaks out of foreign content (§13.2.6.5).
  5219. const hasFontBreakoutAttr = (/** @type {AttributeRun} */ run) => {
  5220. for (let i = run.start; i < run.start + run.count; i++) {
  5221. if (FONT_BREAKOUT_ATTRS.has(_attrNames[i])) return true;
  5222. }
  5223. return false;
  5224. };
  5225. /**
  5226. * Hash of the ASCII-lowercased `name` for the intern tables below; must stay
  5227. * in sync with the range hash in `internLowerName`.
  5228. * @param {string} name lowercase name
  5229. * @returns {number} hash
  5230. */
  5231. const hashLowerName = (name) => {
  5232. let h = name.length;
  5233. for (let i = 0; i < name.length; i++) {
  5234. let c = name.charCodeAt(i);
  5235. if (c >= 0x41 && c <= 0x5a) c += 0x20;
  5236. h = (Math.imul(h, 31) + c) | 0;
  5237. }
  5238. return h;
  5239. };
  5240. // Text-run scan classes for the `skip.text` fast path: 2 = stop the fast
  5241. // path (& / NUL / CR), 1 = ASCII whitespace, 0 = ordinary text.
  5242. const _TEXT_SCAN_CLASS = new Uint8Array(128);
  5243. _TEXT_SCAN_CLASS[0x09] = 1;
  5244. _TEXT_SCAN_CLASS[0x0a] = 1;
  5245. _TEXT_SCAN_CLASS[0x0c] = 1;
  5246. _TEXT_SCAN_CLASS[0x20] = 1;
  5247. _TEXT_SCAN_CLASS[0x26] = 2;
  5248. _TEXT_SCAN_CLASS[0x00] = 2;
  5249. _TEXT_SCAN_CLASS[0x0d] = 2;
  5250. /**
  5251. * @param {Iterable<string>} names lowercase names to intern
  5252. * @returns {{ mask: number, hashes: Int32Array, values: (string | undefined)[], memo: string[], sourceBacked: number[], noted: Uint8Array }} open-addressed intern table
  5253. */
  5254. const buildNameInternTable = (names) => {
  5255. // Open-addressed table (~25% load): the per-name probe is one or two array
  5256. // reads instead of a `Map#get`, and the tables are built once at startup.
  5257. const unique = [...new Set(names)];
  5258. let size = 8;
  5259. while (size < unique.length * 4) size <<= 1;
  5260. const mask = size - 1;
  5261. const hashes = new Int32Array(size);
  5262. /** @type {(string | undefined)[]} */
  5263. const values = Array.from({ length: size });
  5264. for (const name of unique) {
  5265. const h = hashLowerName(name);
  5266. let slot = h & mask;
  5267. // Two names sharing a hash take two slots rather than one bucket, so a
  5268. // slot holds one string and the lookup's load stays monomorphic.
  5269. while (values[slot] !== undefined) slot = (slot + 1) & mask;
  5270. hashes[slot] = h;
  5271. values[slot] = name;
  5272. }
  5273. // `memo` short-circuits the hash walk for a name seen a moment ago (see
  5274. // `internLowerName`); "" marks an empty slot so the load stays monomorphic.
  5275. // `sourceBacked` lists the slots holding a slice of the parsed input — the
  5276. // only ones the next parse drops — and `noted` keeps each in it once.
  5277. return {
  5278. mask,
  5279. hashes,
  5280. values,
  5281. memo: Array.from({ length: 512 }).fill(""),
  5282. sourceBacked: [],
  5283. noted: new Uint8Array(512)
  5284. };
  5285. };
  5286. /** @typedef {ReturnType<typeof buildNameInternTable>} NameInternTable */
  5287. /**
  5288. * Drop the memo entries holding a slice of the last parsed input, so no parse
  5289. * retains another's source. Past the memo's own size a full clear is cheaper.
  5290. * @param {NameInternTable} table intern table
  5291. * @returns {void}
  5292. */
  5293. const _dropSourceBackedNames = (table) => {
  5294. const slots = table.sourceBacked;
  5295. if (slots.length === 0) return;
  5296. const { memo, noted } = table;
  5297. // A slot a known name has since claimed is dropped with the rest: the entry is
  5298. // safe to keep, but noting that costs a write on the path every name takes.
  5299. if (slots.length >= memo.length) {
  5300. memo.fill("");
  5301. noted.fill(0);
  5302. } else {
  5303. for (const slot of slots) {
  5304. memo[slot] = "";
  5305. noted[slot] = 0;
  5306. }
  5307. }
  5308. slots.length = 0;
  5309. };
  5310. /**
  5311. * The lowercased name for `input[start..end)`, returning the shared interned
  5312. * string for known names — skipping the per-tag `slice().toLowerCase()`
  5313. * allocation, and making the tree builder's many Set/Map lookups and `===`
  5314. * comparisons on the name hit one string instance with a cached hash.
  5315. * @param {NameInternTable} table intern table
  5316. * @param {string} input source text
  5317. * @param {number} start name start
  5318. * @param {number} end name end
  5319. * @returns {string} lowercased name
  5320. */
  5321. const internLowerName = (table, input, start, end) => {
  5322. // A document spells the same few names over and over, so a direct-mapped
  5323. // memo keyed on folded first char + length answers most calls with one
  5324. // verified compare instead of the full hash walk. Every hit re-verifies
  5325. // against the current range, so a collision only ever misses, never lies.
  5326. let c0 = input.charCodeAt(start);
  5327. if (c0 >= 0x41 && c0 <= 0x5a) c0 += 0x20;
  5328. const memo = table.memo;
  5329. const memoSlot = (Math.imul(c0, 31) + (end - start)) & 511;
  5330. const cached = memo[memoSlot];
  5331. if (cached.length !== 0 && rangeEqualsLowerCase(input, start, end, cached)) {
  5332. return cached;
  5333. }
  5334. let h = end - start;
  5335. for (let i = start; i < end; i++) {
  5336. let c = input.charCodeAt(i);
  5337. if (c >= 0x41 && c <= 0x5a) c += 0x20;
  5338. h = (Math.imul(h, 31) + c) | 0;
  5339. }
  5340. const { mask, hashes, values } = table;
  5341. let slot = h & mask;
  5342. for (;;) {
  5343. const hit = values[slot];
  5344. // Only an empty slot ends the walk: a name that hashes the same as another
  5345. // sits one slot along, so an equal hash that does not match is not a miss.
  5346. if (hit === undefined) break;
  5347. if (hashes[slot] === h && rangeEqualsLowerCase(input, start, end, hit)) {
  5348. return (memo[memoSlot] = hit);
  5349. }
  5350. slot = (slot + 1) & mask;
  5351. }
  5352. // Unknown name (custom element, data-* attribute, non-ASCII, …). Only ASCII
  5353. // folds — a Unicode `toLowerCase` maps U+212A onto `k` and U+0130 onto two
  5354. // characters, neither of which the tokenizer does.
  5355. if (table.noted[memoSlot] === 0) {
  5356. table.noted[memoSlot] = 1;
  5357. table.sourceBacked.push(memoSlot);
  5358. }
  5359. return (memo[memoSlot] = _asciiLowerCase(input.slice(start, end)));
  5360. };
  5361. // Every tag name the tree builder compares against (the sets above already
  5362. // cover most of the spec), plus the remaining standard/foreign names so
  5363. // ordinary documents intern every tag.
  5364. const TAG_NAME_INTERN = buildNameInternTable([
  5365. ...VOID,
  5366. ...SPECIAL,
  5367. ...FORMATTING,
  5368. ...HEADING,
  5369. ...MATHML_TEXT_INTEGRATION,
  5370. ...MATHML_SPECIAL,
  5371. ...SVG_SPECIAL,
  5372. ...HTML_SCOPE,
  5373. ...TABLE_CONTEXT,
  5374. ...VOID_FORMATTING,
  5375. ...NO_DECODE_TEXT,
  5376. ...FOREIGN_BREAKOUT,
  5377. ...HEAD_ELEMENTS,
  5378. ...Object.keys(SVG_TAG_ADJUST),
  5379. ..."a abbr audio bdi bdo canvas cite data datalist del dfn dialog ins kbd label legend map mark math menuitem meter optgroup option output picture progress q rb rp rt rtc ruby samp selectedcontent slot span sub sup svg time u var video".split(
  5380. " "
  5381. )
  5382. ]);
  5383. // Common attribute names (unknown ones — data-*, ARIA, events — fall back).
  5384. const ATTR_NAME_INTERN = buildNameInternTable(
  5385. "href src srcset sizes alt title class id style name type value content charset rel media target action method placeholder disabled checked selected multiple readonly required hidden tabindex role lang dir width height loading decoding async defer integrity crossorigin referrerpolicy nonce as for colspan rowspan span label max min step pattern autocomplete autofocus autoplay controls loop muted poster preload download ping imagesrcset imagesizes slot part is property http-equiv accept enctype novalidate maxlength minlength size cols rows wrap open scope headers datetime cite usemap ismap shape coords start reversed face color encoding xmlns".split(
  5386. " "
  5387. )
  5388. );
  5389. // Hoisted so the many `open.some(...)` "is there an open HTML <template>?"
  5390. // checks reuse one predicate instead of allocating an arrow per call.
  5391. const isHtmlTemplateEl = (/** @type {HtmlElement} */ e) =>
  5392. _tagNameOf(e) === "template" && _namespaceOf(e) === NS_HTML;
  5393. /**
  5394. * Shared empty skip set so the common (no-skip) call allocates nothing.
  5395. * @type {HtmlAstSkip}
  5396. */
  5397. const EMPTY_SKIP = Object.freeze({});
  5398. /**
  5399. * Optional node kinds a consumer can drop from the AST for speed/memory. Each
  5400. * is a pure output reduction — tree construction (and quirks detection) runs
  5401. * unchanged, so element structure and offsets are identical either way.
  5402. * @typedef {object} HtmlAstSkip
  5403. * @property {boolean=} text drop every `Text` node. Raw-text element bodies (`<script>`/`<style>`/…) aren't emitted either — their content span is recorded as the element's `contentEnd` (see `RAW_TEXT_ELEMENTS`) so a consumer can read `[tagEnd, contentEnd]` by offset. For consumers that read text by offset (e.g. `HtmlParser`), never the html5lib serializer.
  5404. * @property {boolean=} comments drop comment nodes entirely. Not for consumers that read comments (e.g. webpack magic comments).
  5405. * @property {boolean=} doctype drop the doctype node; quirks-mode detection is unaffected.
  5406. */
  5407. /**
  5408. * @typedef {object} HtmlParseOptions
  5409. * @property {string=} fragmentContext context element name for fragment parsing (e.g. `td`, `svg path`); omit for a full document
  5410. * @property {HtmlAstSkip=} skip node kinds to omit from the AST (see `HtmlAstSkip`); omit to build the full tree
  5411. */
  5412. /**
  5413. * @param {string} name raw SVG tag name
  5414. * @returns {string} case-adjusted SVG tag name
  5415. */
  5416. const adjustSvgTag = (name) =>
  5417. /** @type {Record<string, string>} */ (SVG_TAG_ADJUST)[name] || name;
  5418. /**
  5419. * @param {string} name doctype name
  5420. * @param {string | null} pub public id
  5421. * @param {string | null} sys system id
  5422. * @returns {boolean} whether the doctype forces quirks mode
  5423. */
  5424. const isQuirky = (name, pub, sys) => {
  5425. if (name !== "html") return true;
  5426. const p = pub ? pub.toLowerCase() : null;
  5427. const sl = sys ? sys.toLowerCase() : null;
  5428. if (p !== null) {
  5429. if (QUIRKY_EXACT.has(p)) return true;
  5430. for (const pre of QUIRKY_PREFIXES) if (p.startsWith(pre)) return true;
  5431. if (
  5432. sl === null &&
  5433. (p.startsWith("-//w3c//dtd html 4.01 frameset//") ||
  5434. p.startsWith("-//w3c//dtd html 4.01 transitional//"))
  5435. ) {
  5436. return true;
  5437. }
  5438. }
  5439. if (sl === "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd") {
  5440. return true;
  5441. }
  5442. return false;
  5443. };
  5444. const mkEl = (
  5445. /** @type {string} */ tagName,
  5446. /** @type {number} */ ns,
  5447. /** @type {AttributeRun} */ attrs,
  5448. /** @type {TagPos | null | undefined} */ pos
  5449. ) => {
  5450. // Inlined element allocation: every column is written exactly once (the
  5451. // generic _allocNode would zero five columns mkEl immediately overwrites).
  5452. const el = ++_nodeCount;
  5453. if (el >= _nodeCapacity) _growNodeColumns(el + 1);
  5454. _nodeTypes[el] = NodeType.Element;
  5455. // Void HTML elements are marked self-closing and never receive children.
  5456. _nodeFlags[el] =
  5457. ns === NS_HTML && VOID.has(tagName) ? ns | FLAG_SELF_CLOSING : ns;
  5458. if (pos) {
  5459. _nodeStarts[el] = pos.start;
  5460. _nodeEnds[el] = pos.end;
  5461. _nodeTagEnds[el] = pos.tagEnd;
  5462. _nodeNameEnds[el] = pos.nameEnd;
  5463. // End of a raw-text element's body under `skip.text` (defaults to the
  5464. // body start, i.e. empty); lets consumers read `<script>`/`<style>`
  5465. // content as [`tagEnd`, `contentEnd`] without a `Text` node.
  5466. _nodeContentEnds[el] = pos.tagEnd;
  5467. } else {
  5468. _nodeStarts[el] = 0;
  5469. _nodeEnds[el] = 0;
  5470. _nodeTagEnds[el] = 0;
  5471. _nodeNameEnds[el] = 0;
  5472. _nodeContentEnds[el] = 0;
  5473. }
  5474. _nodeParents[el] = 0;
  5475. _nodeFirstChildren[el] = 0;
  5476. _nodeLastChildren[el] = 0;
  5477. _nodeNextSiblings[el] = 0;
  5478. _nodeAttrStarts[el] = attrs.start;
  5479. _nodeAttrCounts[el] = attrs.count;
  5480. _nodeStrings[el] = tagName;
  5481. return el;
  5482. };
  5483. /**
  5484. * A `<template>`'s children live in its content fragment; inserting into a
  5485. * template really inserts there (the spec's "template contents" redirect).
  5486. * @param {HtmlNodeRef} parent container
  5487. * @returns {HtmlNodeRef} effective container to link children into
  5488. */
  5489. const effParent = (parent) => {
  5490. const tc = _templateContentOf(parent);
  5491. return tc !== 0 ? tc : parent;
  5492. };
  5493. const isScopeBoundary = (/** @type {HtmlElement} */ el) => {
  5494. if (_namespaceOf(el) === NS_HTML) return HTML_SCOPE.has(_tagNameOf(el));
  5495. if (_namespaceOf(el) === NS_MATHML) {
  5496. return MATHML_SPECIAL.has(_tagNameOf(el));
  5497. }
  5498. if (_namespaceOf(el) === NS_SVG) {
  5499. return SVG_SPECIAL.has(_tagNameOf(el).toLowerCase());
  5500. }
  5501. return false;
  5502. };
  5503. // Attribute values are stored raw (offsets into the source), so two unresolved
  5504. // values compare by range — `_attrValueOf` would slice a string per side just
  5505. // to throw it away.
  5506. /** @type {(i: number, j: number) => boolean} */
  5507. const _attrValueEquals = (i, j) => {
  5508. if (_attrValues[i] === null && _attrValues[j] === null) {
  5509. const start = _attrValueStarts[i];
  5510. const len = _attrValueEnds[i] - start;
  5511. const otherStart = _attrValueStarts[j];
  5512. if (len !== _attrValueEnds[j] - otherStart) return false;
  5513. for (let k = 0; k < len; k++) {
  5514. if (
  5515. _htmlSource.charCodeAt(start + k) !==
  5516. _htmlSource.charCodeAt(otherStart + k)
  5517. ) {
  5518. return false;
  5519. }
  5520. }
  5521. return true;
  5522. }
  5523. return _attrValueOf(i) === _attrValueOf(j);
  5524. };
  5525. const sameAttrs = (
  5526. /** @type {HtmlElement} */ a,
  5527. /** @type {HtmlElement} */ b
  5528. ) => {
  5529. const aStart = _nodeAttrStarts[a];
  5530. const aCount = _nodeAttrCounts[a];
  5531. const bStart = _nodeAttrStarts[b];
  5532. const bCount = _nodeAttrCounts[b];
  5533. if (aCount !== bCount) return false;
  5534. // Names are unique (deduped), counts tiny — a nested scan beats a Map
  5535. // here on this formatting-element hot path.
  5536. for (let i = bStart; i < bStart + bCount; i++) {
  5537. const j = _findAttr(aStart, aCount, _attrNames[i]);
  5538. if (j === 0 || !_attrValueEquals(j, i)) return false;
  5539. }
  5540. return true;
  5541. };
  5542. const startsWithWs = (/** @type {string} */ s) => {
  5543. const c = s.charCodeAt(0);
  5544. return c === 0x09 || c === 0x0a || c === 0x0c || c === 0x0d || c === 0x20;
  5545. };
  5546. const isAllWs = (/** @type {string} */ s) => {
  5547. for (let i = 0; i < s.length; i++) {
  5548. const c = s.charCodeAt(i);
  5549. // HTML whitespace: tab / LF / FF / CR / space. A charCodeAt loop avoids
  5550. // the `for…of` code-point iterator + per-char string + Set lookup.
  5551. if (c !== 0x09 && c !== 0x0a && c !== 0x0c && c !== 0x0d && c !== 0x20) {
  5552. return false;
  5553. }
  5554. }
  5555. return true;
  5556. };
  5557. // A whitespace run the printer can shorten: two of them in a row, or a lone one
  5558. // that is not already a space.
  5559. const _COLLAPSIBLE_WHITESPACE_REGEXP = /[\t\n\f\r]|[ ][ ]/;
  5560. /**
  5561. * Collapse every run of HTML whitespace to one space — never remove one:
  5562. * `a <b>c</b>` and `a<b>c</b>` render differently.
  5563. * @param {string} s text data
  5564. * @returns {string} the collapsed text
  5565. */
  5566. const collapseWhitespaceRuns = (s) => {
  5567. // Most text collapses to itself, and one scan answers that before the walk
  5568. // below reaches its first run.
  5569. if (!_COLLAPSIBLE_WHITESPACE_REGEXP.test(s)) return s;
  5570. let out = "";
  5571. let last = 0;
  5572. let run = -1;
  5573. for (let i = 0; i <= s.length; i++) {
  5574. const c = i === s.length ? 0 : s.charCodeAt(i);
  5575. const isWhitespace =
  5576. c === 0x09 || c === 0x0a || c === 0x0c || c === 0x0d || c === 0x20;
  5577. if (isWhitespace) {
  5578. if (run === -1) run = i;
  5579. continue;
  5580. }
  5581. // A one-character run is already a single space unless it is a tab or a
  5582. // newline, which still print shorter as one.
  5583. if (run !== -1 && (i - run > 1 || s.charCodeAt(run) !== 0x20)) {
  5584. out += `${s.slice(last, run)} `;
  5585. last = i;
  5586. }
  5587. run = -1;
  5588. }
  5589. return last === 0 ? s : out + s.slice(last);
  5590. };
  5591. const mathmlTextIntegrationPoint = (/** @type {HtmlElement} */ el) =>
  5592. _namespaceOf(el) === NS_MATHML && MATHML_TEXT_INTEGRATION.has(_tagNameOf(el));
  5593. const htmlIntegrationPoint = (/** @type {HtmlElement} */ el) => {
  5594. if (_namespaceOf(el) === NS_MATHML && _tagNameOf(el) === "annotation-xml") {
  5595. const enc = _findAttr(_nodeAttrStarts[el], _nodeAttrCounts[el], "encoding");
  5596. if (enc !== 0) {
  5597. const value = _attrValueOf(enc).toLowerCase();
  5598. if (value === "text/html" || value === "application/xhtml+xml") {
  5599. return true;
  5600. }
  5601. }
  5602. return false;
  5603. }
  5604. if (
  5605. _namespaceOf(el) === NS_SVG &&
  5606. SVG_SPECIAL.has(_tagNameOf(el).toLowerCase())
  5607. ) {
  5608. return true;
  5609. }
  5610. return false;
  5611. };
  5612. // Adjust a foreign start tag's attribute run in place (the run is consumed
  5613. // only by this tag's element): SVG camelCase names are rewritten, and
  5614. // namespaced names get the serializer-name flag (see `_attrSerializedName`).
  5615. const adjustForeignAttrs = (
  5616. /** @type {AttributeRun} */ run,
  5617. /** @type {number} */ ns
  5618. ) => {
  5619. for (let i = run.start; i < run.start + run.count; i++) {
  5620. const name = _attrNames[i];
  5621. if (
  5622. ns === NS_SVG &&
  5623. /** @type {Record<string, string>} */ (SVG_ATTR_ADJUST)[name]
  5624. ) {
  5625. _attrNames[i] = /** @type {Record<string, string>} */ (SVG_ATTR_ADJUST)[
  5626. name
  5627. ];
  5628. }
  5629. if (/** @type {Record<string, string>} */ (FOREIGN_ATTR_NS)[name]) {
  5630. _attrFlags[i] |= 1;
  5631. }
  5632. }
  5633. return run;
  5634. };
  5635. // Only `definitionurl` is rewritten (in place — the run is consumed only by
  5636. // this tag's element); the camelCase name serializes as itself.
  5637. const adjustMathmlAttrs = (/** @type {AttributeRun} */ run) => {
  5638. for (let i = run.start; i < run.start + run.count; i++) {
  5639. if (_attrNames[i] === "definitionurl") _attrNames[i] = "definitionURL";
  5640. }
  5641. return run;
  5642. };
  5643. // ---- HTML tree-construction parser state ----
  5644. // One active parse at a time (results are valid only until the next
  5645. // `parseHtml`); kept at module scope so the helpers below are defined once
  5646. // instead of re-created as closures on every parse.
  5647. let source = "";
  5648. // Whole-document properties, so a substring cannot contain what the input does
  5649. // not. Decoding cannot introduce either: a numeric reference to 0 yields U+FFFD,
  5650. // and CR normalization runs on the raw slice before decoding. Default to `true`
  5651. // so an unset flag only ever costs the old scan.
  5652. let inputHasCr = true;
  5653. let inputHasNul = true;
  5654. let skipText = false;
  5655. let skipComments = false;
  5656. let skipDoctype = false;
  5657. /** @type {HtmlNodeRef} */
  5658. let doc = 0;
  5659. let mode = 0;
  5660. let originalMode = 0;
  5661. /** @type {HtmlElement[]} stack of open elements (bottom .. top) */
  5662. const open = [];
  5663. /** @type {HtmlNodeRef[]} active formatting elements (AFE_MARKER = marker) */
  5664. const afe = [];
  5665. /** @type {HtmlElement} 0 = none */
  5666. let head = 0;
  5667. /** @type {HtmlElement} 0 = none */
  5668. let form = 0;
  5669. let framesetOk = true;
  5670. let pendAttrStart = 0;
  5671. let fosterParenting = false;
  5672. /** @type {number[]} */
  5673. const templateModes = [];
  5674. let quirks = false;
  5675. /** @type {HtmlElement} fragment context element (0 = document parse) */
  5676. let fragment = 0;
  5677. let tokenEnd = 0;
  5678. let swallowNextNewline = false;
  5679. let eofInTag = false;
  5680. let sawSelectedContent = false;
  5681. const decode = decodeEntities;
  5682. /** @type {MutableToken} */
  5683. const tok = {
  5684. type: TOKEN_EOF,
  5685. name: "",
  5686. data: "",
  5687. attrs: { start: 0, count: 0 },
  5688. selfClosing: false,
  5689. start: 0,
  5690. end: 0,
  5691. publicId: null,
  5692. systemId: null,
  5693. forceQuirks: false,
  5694. swallowNewline: false,
  5695. pos: { start: 0, end: 0, tagEnd: 0, nameEnd: 0 }
  5696. };
  5697. const cur = () =>
  5698. /** @type {HtmlElement} */ (open.length > 0 ? open[open.length - 1] : 0);
  5699. /**
  5700. * Remove an element from the middle of the open element stack. A removal that
  5701. * leaves still-open descendants above it breaks the streamed walk whether or
  5702. * not the walk entered the element: it drops off the stack without its subtree
  5703. * ending, so those descendants would be entered under a parent the walk has no
  5704. * open-stack evidence for. Removing the top is just a pop and keeps streaming —
  5705. * which is the common `</form>` after implied end tags.
  5706. * @param {number} index position in `open` to remove
  5707. * @returns {void}
  5708. */
  5709. const removeOpenAt = (index) => {
  5710. open.splice(index, 1);
  5711. _openSpliced = true;
  5712. _openStackChanged = true;
  5713. if (_streaming && index < open.length) _streamHalted = true;
  5714. };
  5715. /**
  5716. * Pop the open element stack. Every pop routes through here so the streamed
  5717. * walk has a single completion point — the HTML analog of the CSS parser's
  5718. * `onRule` sink — instead of being polled once per token.
  5719. * @returns {HtmlElement} the popped element
  5720. */
  5721. const popOpen = () => {
  5722. const el = /** @type {HtmlElement} */ (open.pop());
  5723. // Only mark: an element's end offset is assigned after it is popped, so the
  5724. // walk has to wait for the token to finish.
  5725. _openStackChanged = true;
  5726. return el;
  5727. };
  5728. const adjustedCurrent = () => {
  5729. if (open.length === 1 && fragment) return fragment;
  5730. return cur();
  5731. };
  5732. const appendTo = (
  5733. /** @type {HtmlNodeRef} */ parent,
  5734. /** @type {HtmlNodeRef} */ node
  5735. ) => {
  5736. const p = effParent(parent);
  5737. const last = _nodeLastChildren[p];
  5738. if (
  5739. _nodeTypes[node] === NodeType.Text &&
  5740. last !== 0 &&
  5741. _nodeTypes[last] === NodeType.Text
  5742. ) {
  5743. _nodeStrings[last] += _nodeStrings[node];
  5744. _nodeEnds[last] = _nodeEnds[node];
  5745. return;
  5746. }
  5747. _appendChild(p, node);
  5748. };
  5749. // Reused result of `appropriatePlace` — consumed synchronously by
  5750. // `insertAtPlace` and never retained, so one shared object avoids an
  5751. // allocation per inserted node.
  5752. /** @type {InsertionPlace} */
  5753. const sharedPlace = { parent: doc, beforeNode: 0 };
  5754. const placeAt = (
  5755. /** @type {HtmlNodeRef} */ parent,
  5756. /** @type {HtmlNodeRef} */ beforeNode
  5757. ) => {
  5758. sharedPlace.parent = parent;
  5759. sharedPlace.beforeNode = beforeNode;
  5760. return sharedPlace;
  5761. };
  5762. /**
  5763. * §13.2.6.1 "appropriate place for inserting a node", the foster-parenting
  5764. * branch: where a node goes when the target is a table context — inside the
  5765. * innermost `<template>` when one stands above the innermost `<table>`, else
  5766. * before that table, and at the root when neither is open.
  5767. * @returns {[HtmlNodeRef, HtmlNodeRef]} the parent and the node to insert before (`0` = append)
  5768. */
  5769. const _fosterPlace = () => {
  5770. // The innermost of each still open, found in one walk down the stack.
  5771. let lastTemplate = -1;
  5772. let lastTable = -1;
  5773. for (let i = open.length - 1; i >= 0; i--) {
  5774. if (_namespaceOf(open[i]) !== NS_HTML) continue;
  5775. const name = _tagNameOf(open[i]);
  5776. if (lastTemplate === -1 && name === "template") lastTemplate = i;
  5777. else if (lastTable === -1 && name === "table") lastTable = i;
  5778. }
  5779. if (lastTemplate !== -1 && (lastTable === -1 || lastTemplate > lastTable)) {
  5780. return [open[lastTemplate], 0];
  5781. }
  5782. if (lastTable === -1) return [open[0], 0];
  5783. const table = open[lastTable];
  5784. const parent = _nodeParents[table];
  5785. // A table already taken out of the tree fosters before the element that
  5786. // stood under it instead.
  5787. return parent !== 0 ? [parent, table] : [open[lastTable - 1], 0];
  5788. };
  5789. // "appropriate place for inserting a node"
  5790. const appropriatePlace = () => {
  5791. const target = cur();
  5792. if (
  5793. fosterParenting &&
  5794. TABLE_CONTEXT.has(_tagNameOf(target)) &&
  5795. _namespaceOf(target) === NS_HTML
  5796. ) {
  5797. const [parent, beforeNode] = _fosterPlace();
  5798. return placeAt(parent, beforeNode);
  5799. }
  5800. return placeAt(target, 0);
  5801. };
  5802. const insertAtPlace = (
  5803. /** @type {InsertionPlace} */ place,
  5804. /** @type {HtmlNodeRef} */ node
  5805. ) => {
  5806. const before = place.beforeNode;
  5807. if (before !== 0) {
  5808. const p = effParent(place.parent);
  5809. // Find `before`'s previous sibling (insert-before is a rare foster/
  5810. // adoption path, so the sibling scan stays off the hot path).
  5811. let prev = 0;
  5812. let c = _nodeFirstChildren[p];
  5813. while (c !== 0 && c !== before) {
  5814. prev = c;
  5815. c = _nodeNextSiblings[c];
  5816. }
  5817. if (c === 0) {
  5818. // `before` not under `parent` (not reachable from the spec paths).
  5819. appendTo(place.parent, node);
  5820. return;
  5821. }
  5822. if (
  5823. _nodeTypes[node] === NodeType.Text &&
  5824. prev !== 0 &&
  5825. _nodeTypes[prev] === NodeType.Text
  5826. ) {
  5827. // Before-node merge deliberately does not bump the sibling's `end`.
  5828. _nodeStrings[prev] += _nodeStrings[node];
  5829. return;
  5830. }
  5831. _nodeParents[node] = p;
  5832. _nodeNextSiblings[node] = before;
  5833. if (prev === 0) _nodeFirstChildren[p] = node;
  5834. else _nodeNextSiblings[prev] = node;
  5835. if (_namespaceOf(before) === NS_HTML && _tagNameOf(before) === "table") {
  5836. _recordFostered(before, node);
  5837. }
  5838. } else {
  5839. appendTo(place.parent, node);
  5840. }
  5841. };
  5842. /**
  5843. * Remember that `node` was foster parented out of `table`. The parent it landed
  5844. * in is not recorded: the adoption agency can move it elsewhere afterwards, so
  5845. * only the tree answers that.
  5846. * @param {HtmlNodeRef} table the table it was written inside
  5847. * @param {HtmlNodeRef} node the fostered node
  5848. * @returns {void}
  5849. */
  5850. const _recordFostered = (table, node) => {
  5851. if (_fosteredLog === null) _fosteredLog = [];
  5852. _fosteredLog.push(table, node);
  5853. };
  5854. const insertCharacters = (
  5855. /** @type {string} */ data,
  5856. /** @type {number} */ start,
  5857. /** @type {number} */ end
  5858. ) => {
  5859. const place = appropriatePlace();
  5860. if (_nodeTypes[place.parent] === NodeType.Document) return; // never insert text into document
  5861. // `skip.text`: drop every `Text` node — construction already used the
  5862. // decoded token, so removing the node never affects element structure.
  5863. // For a raw-text element record the body end so a consumer reads the span
  5864. // [`tagEnd`, `contentEnd`] without a `Text` node (see `HtmlParser`).
  5865. // Namespace-agnostic: `HtmlParser` extracts `<script>`/`<style>` bodies in
  5866. // foreign content (e.g. SVG `<style>`) too.
  5867. if (skipText) {
  5868. const p = place.parent;
  5869. if (
  5870. _nodeTypes[p] === NodeType.Element &&
  5871. RAW_TEXT_ELEMENTS.has(_tagNameOf(p))
  5872. ) {
  5873. _nodeContentEnds[p] = end;
  5874. }
  5875. return;
  5876. }
  5877. // Inlined text insert: when the run merges into the adjacent text sibling
  5878. // (common with inline formatting) only the string is appended — no
  5879. // throwaway text node is allocated. Mirrors `insertAtPlace`/`appendTo`,
  5880. // including that the before-node merge does not bump `end`.
  5881. const p = effParent(place.parent);
  5882. const before = place.beforeNode;
  5883. if (before !== 0) {
  5884. let prev = 0;
  5885. let c = _nodeFirstChildren[p];
  5886. while (c !== 0 && c !== before) {
  5887. prev = c;
  5888. c = _nodeNextSiblings[c];
  5889. }
  5890. if (c === 0) {
  5891. // `before` not under `parent` (not reachable from the spec paths).
  5892. _appendChild(p, _makeTextNode(data, start, end));
  5893. return;
  5894. }
  5895. if (prev !== 0 && _nodeTypes[prev] === NodeType.Text) {
  5896. _nodeStrings[prev] += data;
  5897. return;
  5898. }
  5899. const node = _makeTextNode(data, start, end);
  5900. _nodeParents[node] = p;
  5901. _nodeNextSiblings[node] = before;
  5902. if (prev === 0) _nodeFirstChildren[p] = node;
  5903. else _nodeNextSiblings[prev] = node;
  5904. if (_namespaceOf(before) === NS_HTML && _tagNameOf(before) === "table") {
  5905. _recordFostered(before, node);
  5906. }
  5907. } else {
  5908. const last = _nodeLastChildren[p];
  5909. if (last !== 0 && _nodeTypes[last] === NodeType.Text) {
  5910. _nodeStrings[last] += data;
  5911. _nodeEnds[last] = end;
  5912. return;
  5913. }
  5914. _appendChild(p, _makeTextNode(data, start, end));
  5915. }
  5916. };
  5917. /**
  5918. * @param {CommentToken | ProcessingInstructionToken} t comment or processing instruction token
  5919. * @param {InsertionPlace=} place explicit insertion place
  5920. */
  5921. // Every insertion mode inserts a processing instruction where it inserts a
  5922. // comment (§13.2.6.4). `skip.comments` drops comments alone.
  5923. const insertCommentOrProcessingInstruction = (t, place) => {
  5924. const isPi = t.type === TOKEN_PROCESSING_INSTRUCTION;
  5925. if (skipComments && !isPi) return;
  5926. const p = place || appropriatePlace();
  5927. insertAtPlace(
  5928. p,
  5929. isPi
  5930. ? _makeProcessingInstructionNode(t.name, t.data, t.start, t.end)
  5931. : _makeCommentNode(t.data, t.start, t.end)
  5932. );
  5933. };
  5934. const insertHtmlElement = (
  5935. /** @type {string} */ tagName,
  5936. /** @type {AttributeRun} */ attrs,
  5937. /** @type {TagPos | null} */ pos
  5938. ) => {
  5939. const el = mkEl(tagName, NS_HTML, attrs, pos);
  5940. const place = appropriatePlace();
  5941. insertAtPlace(place, el);
  5942. open.push(el);
  5943. return el;
  5944. };
  5945. const insertForeignElement = (
  5946. /** @type {string} */ tagName,
  5947. /** @type {number} */ ns,
  5948. /** @type {AttributeRun} */ attrs,
  5949. /** @type {TagPos | null} */ pos
  5950. ) => {
  5951. const el = mkEl(tagName, ns, attrs, pos);
  5952. const place = appropriatePlace();
  5953. insertAtPlace(place, el);
  5954. open.push(el);
  5955. return el;
  5956. };
  5957. // ---- scopes ----
  5958. // Scope "kind" selects which extra elements act as boundaries. Passed as a
  5959. // small int so the scope checks below allocate no per-call predicate closure
  5960. // (these run several times per body tag).
  5961. const SCOPE_DEFAULT = 0;
  5962. const SCOPE_BUTTON = 1;
  5963. const SCOPE_LIST_ITEM = 2;
  5964. const isBoundaryForKind = (
  5965. /** @type {HtmlElement} */ el,
  5966. /** @type {number} */ kind
  5967. ) => {
  5968. if (isScopeBoundary(el)) return true;
  5969. if (_namespaceOf(el) !== NS_HTML) return false;
  5970. if (kind === SCOPE_BUTTON) return _tagNameOf(el) === "button";
  5971. if (kind === SCOPE_LIST_ITEM) {
  5972. return _tagNameOf(el) === "ol" || _tagNameOf(el) === "ul";
  5973. }
  5974. return false;
  5975. };
  5976. // "have an element in scope": walk the open stack from the top until the
  5977. // named HTML element is found (true) or a scope boundary is hit (false).
  5978. const hasNameInScope = (
  5979. /** @type {string} */ tagName,
  5980. /** @type {number} */ kind
  5981. ) => {
  5982. for (let i = open.length - 1; i >= 0; i--) {
  5983. const el = open[i];
  5984. if (_namespaceOf(el) === NS_HTML && _tagNameOf(el) === tagName) {
  5985. return true;
  5986. }
  5987. if (isBoundaryForKind(el, kind)) return false;
  5988. }
  5989. return false;
  5990. };
  5991. const inScope = (/** @type {string} */ tagName) =>
  5992. hasNameInScope(tagName, SCOPE_DEFAULT);
  5993. const inButtonScope = (/** @type {string} */ tagName) =>
  5994. hasNameInScope(tagName, SCOPE_BUTTON);
  5995. const inListItemScope = (/** @type {string} */ tagName) =>
  5996. hasNameInScope(tagName, SCOPE_LIST_ITEM);
  5997. const inScopeEl = (/** @type {HtmlElement} */ target) => {
  5998. for (let i = open.length - 1; i >= 0; i--) {
  5999. const el = open[i];
  6000. if (el === target) return true;
  6001. if (isScopeBoundary(el)) return false;
  6002. }
  6003. return false;
  6004. };
  6005. // `target` is a single tag name (the common case) or a Set of names.
  6006. const inTableScope = (/** @type {string | Set<string>} */ target) => {
  6007. const set = typeof target === "string" ? null : target;
  6008. for (let i = open.length - 1; i >= 0; i--) {
  6009. const el = open[i];
  6010. if (_namespaceOf(el) === NS_HTML) {
  6011. if (set ? set.has(_tagNameOf(el)) : _tagNameOf(el) === target) {
  6012. return true;
  6013. }
  6014. if (TABLE_SCOPE_STOP.has(_tagNameOf(el))) return false;
  6015. }
  6016. }
  6017. return false;
  6018. };
  6019. const generateImpliedEndTags = (except = "") => {
  6020. while (open.length) {
  6021. const el = cur();
  6022. if (
  6023. _namespaceOf(el) === NS_HTML &&
  6024. IMPLIED.has(_tagNameOf(el)) &&
  6025. _tagNameOf(el) !== except
  6026. ) {
  6027. popOpen();
  6028. } else {
  6029. break;
  6030. }
  6031. }
  6032. };
  6033. const generateImpliedEndTagsThorough = () => {
  6034. while (open.length) {
  6035. const el = cur();
  6036. if (_namespaceOf(el) === NS_HTML && IMPLIED_THOROUGH.has(_tagNameOf(el))) {
  6037. popOpen();
  6038. } else {
  6039. break;
  6040. }
  6041. }
  6042. };
  6043. // ---- active formatting elements ----
  6044. // `splice` allocates an array for the removed element on every call; the list
  6045. // is short, so shifting in place is both cheaper and allocation-free.
  6046. /** @type {(i: number) => void} */
  6047. const afeRemoveAt = (i) => {
  6048. const last = afe.length - 1;
  6049. for (let k = i; k < last; k++) afe[k] = afe[k + 1];
  6050. afe.pop();
  6051. };
  6052. const pushAfe = (/** @type {HtmlElement} */ el) => {
  6053. let count = 0;
  6054. for (let i = afe.length - 1; i >= 0; i--) {
  6055. const e = afe[i];
  6056. if (e === AFE_MARKER) break;
  6057. if (
  6058. _tagNameOf(e) === _tagNameOf(el) &&
  6059. _namespaceOf(e) === _namespaceOf(el) &&
  6060. sameAttrs(e, el)
  6061. ) {
  6062. count++;
  6063. if (count === 3) {
  6064. afeRemoveAt(i);
  6065. break;
  6066. }
  6067. }
  6068. }
  6069. afe.push(el);
  6070. };
  6071. const insertMarker = () => afe.push(AFE_MARKER);
  6072. const clearAfeToMarker = () => {
  6073. while (afe.length) {
  6074. if (afe.pop() === AFE_MARKER) break;
  6075. }
  6076. };
  6077. const reconstructAfe = () => {
  6078. if (afe.length === 0) return;
  6079. let i = afe.length - 1;
  6080. if (afe[i] === AFE_MARKER || open.includes(afe[i])) return;
  6081. while (i > 0) {
  6082. i--;
  6083. if (afe[i] === AFE_MARKER || open.includes(afe[i])) {
  6084. i++;
  6085. break;
  6086. }
  6087. }
  6088. for (; i < afe.length; i++) {
  6089. const e = afe[i];
  6090. // §13.2.6.4.7 gives `a` and `nobr` a start-tag rule of their own; every other
  6091. // formatting element re-nests as written.
  6092. _printFromSource = true;
  6093. const el = mkEl(_tagNameOf(e), _namespaceOf(e), cloneAttrs(e), null);
  6094. const place = appropriatePlace();
  6095. insertAtPlace(place, el);
  6096. open.push(el);
  6097. afe[i] = el;
  6098. }
  6099. };
  6100. // ---- close p ----
  6101. const closePElement = () => {
  6102. generateImpliedEndTags("p");
  6103. // pop until a p has been popped
  6104. while (open.length) {
  6105. const el = /** @type {HtmlElement} */ (popOpen());
  6106. _nodeEnds[el] = tokenEnd;
  6107. if (_namespaceOf(el) === NS_HTML && _tagNameOf(el) === "p") {
  6108. break;
  6109. }
  6110. }
  6111. };
  6112. const popUntil = (/** @type {string} */ tagName) => {
  6113. while (open.length) {
  6114. const el = /** @type {HtmlElement} */ (popOpen());
  6115. _nodeEnds[el] = tokenEnd;
  6116. if (_namespaceOf(el) === NS_HTML && _tagNameOf(el) === tagName) {
  6117. break;
  6118. }
  6119. }
  6120. };
  6121. const popUntilOneOf = (/** @type {Set<string>} */ set) => {
  6122. while (open.length) {
  6123. const el = /** @type {HtmlElement} */ (popOpen());
  6124. _nodeEnds[el] = tokenEnd;
  6125. if (_namespaceOf(el) === NS_HTML && set.has(_tagNameOf(el))) {
  6126. break;
  6127. }
  6128. }
  6129. };
  6130. // ---- reset insertion mode appropriately ----
  6131. const resetInsertionMode = () => {
  6132. let last = false;
  6133. for (let i = open.length - 1; i >= 0; i--) {
  6134. let node = open[i];
  6135. if (i === 0) {
  6136. last = true;
  6137. if (fragment) node = fragment;
  6138. }
  6139. const tn = _tagNameOf(node);
  6140. if (_namespaceOf(node) === NS_HTML) {
  6141. if ((tn === "td" || tn === "th") && !last) {
  6142. mode = MODE_IN_CELL;
  6143. return;
  6144. }
  6145. if (tn === "tr") {
  6146. mode = MODE_IN_ROW;
  6147. return;
  6148. }
  6149. if (TBODY_GROUP.has(tn)) {
  6150. mode = MODE_IN_TABLE_BODY;
  6151. return;
  6152. }
  6153. if (tn === "caption") {
  6154. mode = MODE_IN_CAPTION;
  6155. return;
  6156. }
  6157. if (tn === "colgroup") {
  6158. mode = MODE_IN_COLUMN_GROUP;
  6159. return;
  6160. }
  6161. if (tn === "table") {
  6162. mode = MODE_IN_TABLE;
  6163. return;
  6164. }
  6165. if (tn === "template") {
  6166. mode = templateModes[templateModes.length - 1];
  6167. return;
  6168. }
  6169. if (tn === "head" && !last) {
  6170. mode = MODE_IN_HEAD;
  6171. return;
  6172. }
  6173. if (tn === "body") {
  6174. mode = MODE_IN_BODY;
  6175. return;
  6176. }
  6177. if (tn === "frameset") {
  6178. mode = MODE_IN_FRAMESET;
  6179. return;
  6180. }
  6181. if (tn === "html") {
  6182. mode = head ? MODE_AFTER_HEAD : MODE_BEFORE_HEAD;
  6183. return;
  6184. }
  6185. }
  6186. if (last) {
  6187. mode = MODE_IN_BODY;
  6188. return;
  6189. }
  6190. }
  6191. };
  6192. // ---------- token processing ----------
  6193. // Split a character token's leading whitespace; per the spec each character
  6194. // is its own token, so a mixed run can straddle a mode change. Inserts the
  6195. // leading whitespace when `insert`, returns the non-whitespace remainder
  6196. // token (or null when the token was entirely whitespace).
  6197. const leadingWs = (
  6198. /** @type {CharToken} */ t,
  6199. /** @type {boolean} */ insert
  6200. ) => {
  6201. const m = /^[\t\n\f\r ]+/.exec(t.data);
  6202. const ws = m ? m[0] : "";
  6203. // `t.data` is decoded, the offsets are raw, so counting one in the other
  6204. // only lines up when decoding changed no length (no CRLF, no reference).
  6205. // Otherwise keep the whole span: too wide is harmless, too narrow names
  6206. // source that is not this text (see the `Text` printer).
  6207. const aligned = t.end - t.start === t.data.length;
  6208. if (ws && insert) {
  6209. insertCharacters(ws, t.start, aligned ? t.start + ws.length : t.end);
  6210. }
  6211. if (ws.length === t.data.length) return null;
  6212. return {
  6213. ...t,
  6214. data: t.data.slice(ws.length),
  6215. start: aligned ? t.start + ws.length : t.start
  6216. };
  6217. };
  6218. const process = (/** @type {Token} */ t) => {
  6219. // Track the current token's end so explicit closes can set element `.end`.
  6220. // Dispatch on `type` instead of the `in` operator, which goes megamorphic
  6221. // across the token union and shows up on the per-token hot path.
  6222. const ty = t.type;
  6223. if (ty === TOKEN_START_TAG || ty === TOKEN_END_TAG) tokenEnd = t.pos.end;
  6224. else if (ty !== TOKEN_EOF) tokenEnd = t.end;
  6225. // foreign content dispatch
  6226. const ac = adjustedCurrent();
  6227. const useForeign =
  6228. open.length > 0 &&
  6229. ac &&
  6230. _namespaceOf(ac) !== NS_HTML &&
  6231. ty !== TOKEN_EOF &&
  6232. shouldUseForeignRules(ac, t);
  6233. if (useForeign) {
  6234. foreignContent(t);
  6235. return;
  6236. }
  6237. runMode(t);
  6238. };
  6239. const shouldUseForeignRules = (
  6240. /** @type {HtmlElement} */ ac,
  6241. /** @type {Token} */ t
  6242. ) => {
  6243. if (_namespaceOf(ac) === NS_HTML) return false;
  6244. if (t.type === TOKEN_START_TAG) {
  6245. if (
  6246. mathmlTextIntegrationPoint(ac) &&
  6247. t.name !== "mglyph" &&
  6248. t.name !== "malignmark"
  6249. ) {
  6250. return false;
  6251. }
  6252. if (
  6253. _namespaceOf(ac) === NS_MATHML &&
  6254. _tagNameOf(ac) === "annotation-xml" &&
  6255. t.name === "svg"
  6256. ) {
  6257. return false;
  6258. }
  6259. if (htmlIntegrationPoint(ac)) return false;
  6260. return true;
  6261. }
  6262. if (t.type === TOKEN_CHAR) {
  6263. if (mathmlTextIntegrationPoint(ac)) return false;
  6264. if (htmlIntegrationPoint(ac)) return false;
  6265. return true;
  6266. }
  6267. if (t.type === TOKEN_END_TAG) return true;
  6268. if (isCommentOrProcessingInstruction(t)) return true;
  6269. return false;
  6270. };
  6271. const foreignContent = (/** @type {Token} */ t) => {
  6272. if (t.type === TOKEN_CHAR) {
  6273. const data = inputHasNul ? t.data.replace(/\0/g, "�") : t.data;
  6274. insertCharacters(data, t.start, t.end);
  6275. // eslint-disable-next-line no-control-regex
  6276. if (/[^\t\n\f\r \u0000]/.test(t.data)) framesetOk = false;
  6277. return;
  6278. }
  6279. if (isCommentOrProcessingInstruction(t)) {
  6280. insertCommentOrProcessingInstruction(t);
  6281. return;
  6282. }
  6283. if (t.type === TOKEN_DOCTYPE) return;
  6284. if (t.type === TOKEN_START_TAG) {
  6285. const acn = _namespaceOf(adjustedCurrent());
  6286. if (
  6287. FOREIGN_BREAKOUT.has(t.name) ||
  6288. (t.name === "font" && hasFontBreakoutAttr(t.attrs))
  6289. ) {
  6290. // parse error; pop until integration point / html / mathml-text-integration
  6291. while (open.length > 1) {
  6292. const c = cur();
  6293. if (
  6294. _namespaceOf(c) === NS_HTML ||
  6295. mathmlTextIntegrationPoint(c) ||
  6296. htmlIntegrationPoint(c)
  6297. ) {
  6298. break;
  6299. }
  6300. popOpen();
  6301. }
  6302. runMode(t);
  6303. return;
  6304. }
  6305. const ns = acn;
  6306. let name = t.name;
  6307. let attrs = t.attrs;
  6308. if (ns === NS_SVG) {
  6309. name = adjustSvgTag(name);
  6310. }
  6311. if (ns === NS_MATHML) {
  6312. attrs = adjustMathmlAttrs(attrs);
  6313. }
  6314. attrs = adjustForeignAttrs(attrs, ns);
  6315. insertForeignElement(name, ns, attrs, t.pos);
  6316. if (t.selfClosing) {
  6317. popOpen();
  6318. }
  6319. return;
  6320. }
  6321. if (t.type === TOKEN_END_TAG) {
  6322. if (
  6323. t.name === "script" &&
  6324. _tagNameOf(cur()) === "script" &&
  6325. _namespaceOf(cur()) === NS_SVG
  6326. ) {
  6327. popOpen();
  6328. return;
  6329. }
  6330. // `</p>` and `</br>` break out: pop foreign elements up to the
  6331. // nearest HTML element or integration point, then process in HTML.
  6332. if (t.name === "p" || t.name === "br") {
  6333. while (
  6334. open.length > 1 &&
  6335. _namespaceOf(cur()) !== NS_HTML &&
  6336. !mathmlTextIntegrationPoint(cur()) &&
  6337. !htmlIntegrationPoint(cur())
  6338. ) {
  6339. popOpen();
  6340. }
  6341. runMode(t);
  6342. return;
  6343. }
  6344. // any other end tag
  6345. let i = open.length - 1;
  6346. let node = open[i];
  6347. if (_tagNameOf(node).toLowerCase() !== t.name) {
  6348. /* parse error */
  6349. }
  6350. while (i >= 0) {
  6351. node = open[i];
  6352. if (i === 0) return;
  6353. if (
  6354. _namespaceOf(node) !== NS_HTML &&
  6355. _tagNameOf(node).toLowerCase() === t.name
  6356. ) {
  6357. while (open.length > i) popOpen();
  6358. return;
  6359. }
  6360. i--;
  6361. if (open[i] && _namespaceOf(open[i]) === NS_HTML) {
  6362. runMode(t);
  6363. return;
  6364. }
  6365. }
  6366. }
  6367. };
  6368. // ---------- adoption agency algorithm ----------
  6369. const adoptionAgency = (
  6370. /** @type {string} */ subject,
  6371. /** @type {TagPos | null} */ pos
  6372. ) => {
  6373. // step 1
  6374. const c = cur();
  6375. if (
  6376. _namespaceOf(c) === NS_HTML &&
  6377. _tagNameOf(c) === subject &&
  6378. !afe.includes(c)
  6379. ) {
  6380. popOpen();
  6381. return true;
  6382. }
  6383. let outer = 0;
  6384. while (outer < 8) {
  6385. outer++;
  6386. // find formatting element
  6387. let fmtIdx = -1;
  6388. for (let i = afe.length - 1; i >= 0; i--) {
  6389. if (afe[i] === AFE_MARKER) {
  6390. if (_afeNameBelowMarker(i, subject)) _printFromSource = true;
  6391. break;
  6392. }
  6393. if (_tagNameOf(afe[i]) === subject && _namespaceOf(afe[i]) === NS_HTML) {
  6394. fmtIdx = i;
  6395. break;
  6396. }
  6397. }
  6398. if (fmtIdx === -1) return false; // act as any other end tag
  6399. const fmt = afe[fmtIdx];
  6400. const openIdx = open.indexOf(fmt);
  6401. if (openIdx === -1) {
  6402. afeRemoveAt(fmtIdx);
  6403. return true;
  6404. }
  6405. if (!inScopeEl(fmt)) return true; // parse error, ignore
  6406. // step: furthest block
  6407. let furthestIdx = -1;
  6408. for (let i = openIdx + 1; i < open.length; i++) {
  6409. if (isSpecial(open[i])) {
  6410. furthestIdx = i;
  6411. break;
  6412. }
  6413. }
  6414. if (furthestIdx === -1) {
  6415. while (open.length > openIdx) {
  6416. _nodeEnds[/** @type {HtmlElement} */ (open[open.length - 1])] =
  6417. tokenEnd;
  6418. popOpen();
  6419. }
  6420. afeRemoveAt(fmtIdx);
  6421. return true;
  6422. }
  6423. const furthest = open[furthestIdx];
  6424. const commonAncestor = open[openIdx - 1];
  6425. let bookmark = fmtIdx;
  6426. let node = furthest;
  6427. let lastNode = furthest;
  6428. let nodeIdx = furthestIdx;
  6429. let inner = 0;
  6430. while (true) {
  6431. inner++;
  6432. nodeIdx--;
  6433. node = open[nodeIdx];
  6434. if (node === fmt) break;
  6435. let nodeAfeIdx = afe.indexOf(node);
  6436. if (inner > 3 && nodeAfeIdx !== -1) {
  6437. afeRemoveAt(nodeAfeIdx);
  6438. if (nodeAfeIdx < bookmark) bookmark--;
  6439. nodeAfeIdx = -1;
  6440. }
  6441. if (nodeAfeIdx === -1) {
  6442. removeOpenAt(nodeIdx);
  6443. continue;
  6444. }
  6445. // create clone
  6446. const clone = mkEl(
  6447. _tagNameOf(node),
  6448. _namespaceOf(node),
  6449. cloneAttrs(node),
  6450. null
  6451. );
  6452. afe[nodeAfeIdx] = clone;
  6453. open[nodeIdx] = clone;
  6454. node = clone;
  6455. if (lastNode === furthest) bookmark = nodeAfeIdx + 1;
  6456. // append lastNode to node
  6457. detach(lastNode);
  6458. appendTo(node, lastNode);
  6459. lastNode = node;
  6460. }
  6461. // insert lastNode into common ancestor (with foster parenting)
  6462. detach(lastNode);
  6463. const place = placeForCommonAncestor(commonAncestor);
  6464. insertAtPlace(place, lastNode);
  6465. // create element for fmt token, take children of furthest
  6466. _printFromSource = true;
  6467. const cloneFmt = mkEl(
  6468. _tagNameOf(fmt),
  6469. _namespaceOf(fmt),
  6470. cloneAttrs(fmt),
  6471. null
  6472. );
  6473. // Take all direct children of `furthest` (a template's content fragment
  6474. // deliberately stays put — mirrors childrenOf-less spec behavior here). A
  6475. // declarative shadow root is not one of them: the engine took it out of
  6476. // the tree when it attached, so it stays with the host it hangs off.
  6477. let k = _nodeFirstChildren[furthest];
  6478. _nodeFirstChildren[furthest] = 0;
  6479. _nodeLastChildren[furthest] = 0;
  6480. while (k !== 0) {
  6481. const next = _nodeNextSiblings[k];
  6482. _nodeNextSiblings[k] = 0;
  6483. _nodeParents[k] = 0;
  6484. appendTo(
  6485. (_nodeFlags[k] & FLAG_SHADOW_ROOT) === 0 ? cloneFmt : furthest,
  6486. k
  6487. );
  6488. k = next;
  6489. }
  6490. appendTo(furthest, cloneFmt);
  6491. // remove fmt from afe, insert clone at bookmark
  6492. const curFmtIdx = afe.indexOf(fmt);
  6493. if (curFmtIdx !== -1) {
  6494. afeRemoveAt(curFmtIdx);
  6495. if (curFmtIdx < bookmark) bookmark--;
  6496. }
  6497. afe.splice(bookmark, 0, cloneFmt);
  6498. // remove fmt from open, insert clone below furthest
  6499. const ofi = open.indexOf(fmt);
  6500. if (ofi !== -1) removeOpenAt(ofi);
  6501. const newFurthestIdx = open.indexOf(furthest);
  6502. open.splice(newFurthestIdx + 1, 0, cloneFmt);
  6503. _openSpliced = true;
  6504. _openStackChanged = true;
  6505. }
  6506. return true;
  6507. };
  6508. const placeForCommonAncestor = (/** @type {HtmlElement} */ commonAncestor) => {
  6509. if (
  6510. TABLE_CONTEXT.has(_tagNameOf(commonAncestor)) &&
  6511. _namespaceOf(commonAncestor) === NS_HTML
  6512. ) {
  6513. // The same place `appropriatePlace` fosters to, spelled as its own object:
  6514. // the adoption agency holds this one while it moves nodes about, so the
  6515. // shared one would be overwritten under it.
  6516. const [parent, beforeNode] = _fosterPlace();
  6517. return { parent, beforeNode };
  6518. }
  6519. return { parent: commonAncestor, beforeNode: 0 };
  6520. };
  6521. const detach = (/** @type {HtmlNodeRef} */ node) => {
  6522. const p = _nodeParents[node];
  6523. if (p === 0) return;
  6524. let prev = 0;
  6525. let c = _nodeFirstChildren[p];
  6526. while (c !== 0 && c !== node) {
  6527. prev = c;
  6528. c = _nodeNextSiblings[c];
  6529. }
  6530. if (c === 0) return;
  6531. if (prev === 0) _nodeFirstChildren[p] = _nodeNextSiblings[node];
  6532. else _nodeNextSiblings[prev] = _nodeNextSiblings[node];
  6533. if (_nodeLastChildren[p] === node) _nodeLastChildren[p] = prev;
  6534. _nodeNextSiblings[node] = 0;
  6535. _nodeParents[node] = 0;
  6536. };
  6537. // ---------- insertion modes ----------
  6538. /** @type {Record<string, (t: Token) => void>} */
  6539. const modes = {};
  6540. // Dispatch the current insertion mode. An integer switch (cases ordered by
  6541. // frequency) keeps each `modes.x(t)` call site monomorphic, where a keyed
  6542. // `modes[mode]` load + indirect call would defeat inlining on the per-token
  6543. // hot path.
  6544. const runMode = (/** @type {Token} */ t) => {
  6545. switch (mode) {
  6546. case MODE_IN_BODY:
  6547. return modes.inBody(t);
  6548. case MODE_TEXT:
  6549. return modes.text(t);
  6550. case MODE_IN_CELL:
  6551. return modes.inCell(t);
  6552. case MODE_IN_ROW:
  6553. return modes.inRow(t);
  6554. case MODE_IN_TABLE_BODY:
  6555. return modes.inTableBody(t);
  6556. case MODE_IN_TABLE:
  6557. return modes.inTable(t);
  6558. case MODE_IN_TABLE_TEXT:
  6559. return modes.inTableText(t);
  6560. case MODE_IN_CAPTION:
  6561. return modes.inCaption(t);
  6562. case MODE_IN_COLUMN_GROUP:
  6563. return modes.inColumnGroup(t);
  6564. case MODE_IN_TEMPLATE:
  6565. return modes.inTemplate(t);
  6566. case MODE_IN_HEAD:
  6567. return modes.inHead(t);
  6568. case MODE_IN_HEAD_NOSCRIPT:
  6569. return modes.inHeadNoscript(t);
  6570. case MODE_AFTER_HEAD:
  6571. return modes.afterHead(t);
  6572. case MODE_BEFORE_HEAD:
  6573. return modes.beforeHead(t);
  6574. case MODE_BEFORE_HTML:
  6575. return modes.beforeHtml(t);
  6576. case MODE_INITIAL:
  6577. return modes.initial(t);
  6578. case MODE_AFTER_BODY:
  6579. return modes.afterBody(t);
  6580. case MODE_AFTER_AFTER_BODY:
  6581. return modes.afterAfterBody(t);
  6582. case MODE_IN_FRAMESET:
  6583. return modes.inFrameset(t);
  6584. case MODE_AFTER_FRAMESET:
  6585. return modes.afterFrameset(t);
  6586. // MODE_AFTER_AFTER_FRAMESET — every mode is enumerated, so the last
  6587. // one is the `default` (also satisfies exhaustiveness linting).
  6588. default:
  6589. return modes.afterAfterFrameset(t);
  6590. }
  6591. };
  6592. modes.initial = (t) => {
  6593. if (t.type === TOKEN_CHAR) {
  6594. const r = leadingWs(t, false);
  6595. if (!r) return;
  6596. quirks = true;
  6597. mode = MODE_BEFORE_HTML;
  6598. process(r);
  6599. return;
  6600. }
  6601. if (isCommentOrProcessingInstruction(t)) {
  6602. insertCommentOrProcessingInstruction(t, { parent: doc, beforeNode: 0 });
  6603. return;
  6604. }
  6605. if (t.type === TOKEN_DOCTYPE) {
  6606. // `skip.doctype` drops the node only; quirks detection below is unaffected.
  6607. if (!skipDoctype) {
  6608. const dt = _allocNode(NodeType.Doctype, t.start, t.end);
  6609. _nodeStrings[dt] = t.name;
  6610. // At most one doctype node is ever inserted (later doctype tokens
  6611. // are ignored), so its ids live in two per-parse scalars.
  6612. _doctypePublicId = t.publicId;
  6613. _doctypeSystemId = t.systemId;
  6614. _appendChild(doc, dt);
  6615. }
  6616. quirks = t.forceQuirks || isQuirky(t.name, t.publicId, t.systemId);
  6617. mode = MODE_BEFORE_HTML;
  6618. return;
  6619. }
  6620. quirks = true;
  6621. mode = MODE_BEFORE_HTML;
  6622. process(t);
  6623. };
  6624. modes.beforeHtml = (t) => {
  6625. if (t.type === TOKEN_DOCTYPE) return;
  6626. if (isCommentOrProcessingInstruction(t)) {
  6627. insertCommentOrProcessingInstruction(t, { parent: doc, beforeNode: 0 });
  6628. return;
  6629. }
  6630. if (t.type === TOKEN_CHAR) {
  6631. const r = leadingWs(t, false);
  6632. if (!r) return;
  6633. t = r;
  6634. }
  6635. if (t.type === TOKEN_START_TAG && t.name === "html") {
  6636. const el = mkEl("html", NS_HTML, t.attrs, t.pos);
  6637. _appendChild(doc, el);
  6638. open.push(el);
  6639. mode = MODE_BEFORE_HEAD;
  6640. return;
  6641. }
  6642. if (t.type === TOKEN_END_TAG && !HEAD_BODY_HTML_BR.has(t.name)) {
  6643. return;
  6644. }
  6645. const el = mkEl("html", NS_HTML, EMPTY_ATTRS, null);
  6646. _appendChild(doc, el);
  6647. open.push(el);
  6648. mode = MODE_BEFORE_HEAD;
  6649. process(t);
  6650. };
  6651. modes.beforeHead = (t) => {
  6652. if (t.type === TOKEN_CHAR) {
  6653. const r = leadingWs(t, false);
  6654. if (!r) return;
  6655. t = r;
  6656. }
  6657. if (isCommentOrProcessingInstruction(t)) {
  6658. insertCommentOrProcessingInstruction(t);
  6659. return;
  6660. }
  6661. if (t.type === TOKEN_DOCTYPE) return;
  6662. if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t);
  6663. if (t.type === TOKEN_START_TAG && t.name === "head") {
  6664. head = insertHtmlElement("head", t.attrs, t.pos);
  6665. mode = MODE_IN_HEAD;
  6666. return;
  6667. }
  6668. if (t.type === TOKEN_END_TAG && !HEAD_BODY_HTML_BR.has(t.name)) {
  6669. return;
  6670. }
  6671. head = insertHtmlElement("head", EMPTY_ATTRS, null);
  6672. mode = MODE_IN_HEAD;
  6673. process(t);
  6674. };
  6675. modes.inHead = (t) => {
  6676. if (t.type === TOKEN_CHAR) {
  6677. const r = leadingWs(t, true);
  6678. if (!r) return;
  6679. popOpen();
  6680. mode = MODE_AFTER_HEAD;
  6681. process(r);
  6682. return;
  6683. }
  6684. if (isCommentOrProcessingInstruction(t)) {
  6685. insertCommentOrProcessingInstruction(t);
  6686. return;
  6687. }
  6688. if (t.type === TOKEN_DOCTYPE) return;
  6689. if (t.type === TOKEN_START_TAG) {
  6690. if (t.name === "html") return modes.inBody(t);
  6691. if (HEAD_VOID_ELEMENTS.has(t.name)) {
  6692. insertHtmlElement(t.name, t.attrs, t.pos);
  6693. popOpen();
  6694. return;
  6695. }
  6696. if (t.name === "title") {
  6697. genericRcdata(t);
  6698. return;
  6699. }
  6700. if (NOFRAMES_STYLE_NOSCRIPT.has(t.name)) {
  6701. if (t.name === "noscript") {
  6702. insertHtmlElement("noscript", t.attrs, t.pos);
  6703. mode = MODE_IN_HEAD_NOSCRIPT;
  6704. return;
  6705. }
  6706. genericRawtext(t);
  6707. return;
  6708. }
  6709. if (t.name === "script") {
  6710. genericRawtext(t);
  6711. return;
  6712. }
  6713. if (t.name === "template") {
  6714. insertHtmlElement("template", t.attrs, t.pos);
  6715. insertMarker();
  6716. framesetOk = false;
  6717. mode = MODE_IN_TEMPLATE;
  6718. templateModes.push(MODE_IN_TEMPLATE);
  6719. const el = cur();
  6720. const fragment = _allocNode(NodeType.DocumentFragment, 0, 0);
  6721. _nodeStrings[fragment] = "";
  6722. // Parent link so the iterative walk can ascend out of the content.
  6723. _nodeParents[fragment] = el;
  6724. _nodeContentEnds[el] = fragment;
  6725. _nodeFlags[el] |= FLAG_HAS_TEMPLATE;
  6726. // The engine attaches this content to the parent as a shadow root and
  6727. // drops the template from the tree; the one thing that leaves behind is
  6728. // that the tree construction can no longer move it away from its host.
  6729. const host = _nodeParents[el];
  6730. const shadowRootMode = _findAttr(
  6731. t.attrs.start,
  6732. t.attrs.count,
  6733. "shadowrootmode"
  6734. );
  6735. if (
  6736. shadowRootMode !== 0 &&
  6737. host !== 0 &&
  6738. host !== open[0] &&
  6739. (_nodeFlags[host] & FLAG_SHADOW_HOST) === 0 &&
  6740. _nodeTypes[host] === NodeType.Element &&
  6741. _isShadowHost(host)
  6742. ) {
  6743. const mode = _attrValueOf(shadowRootMode).toLowerCase();
  6744. if (mode === "open" || mode === "closed") {
  6745. _nodeFlags[el] |= FLAG_SHADOW_ROOT;
  6746. _nodeFlags[host] |= FLAG_SHADOW_HOST;
  6747. }
  6748. }
  6749. return;
  6750. }
  6751. if (t.name === "head") return;
  6752. }
  6753. if (t.type === TOKEN_END_TAG) {
  6754. if (t.name === "head") {
  6755. popOpen();
  6756. mode = MODE_AFTER_HEAD;
  6757. return;
  6758. }
  6759. if (BODY_HTML_BR.has(t.name)) {
  6760. /* fallthrough */
  6761. } else if (t.name === "template") {
  6762. if (!open.some(isHtmlTemplateEl)) {
  6763. return;
  6764. }
  6765. generateImpliedEndTagsThorough();
  6766. popUntil("template");
  6767. clearAfeToMarker();
  6768. templateModes.pop();
  6769. resetInsertionMode();
  6770. return;
  6771. } else {
  6772. return;
  6773. }
  6774. }
  6775. // anything else
  6776. popOpen();
  6777. mode = MODE_AFTER_HEAD;
  6778. process(t);
  6779. };
  6780. modes.inHeadNoscript = (t) => {
  6781. if (t.type === TOKEN_DOCTYPE) return;
  6782. if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t);
  6783. if (t.type === TOKEN_END_TAG && t.name === "noscript") {
  6784. popOpen();
  6785. mode = MODE_IN_HEAD;
  6786. return;
  6787. }
  6788. if (t.type === TOKEN_CHAR && isAllWs(t.data)) return modes.inHead(t);
  6789. if (isCommentOrProcessingInstruction(t)) return modes.inHead(t);
  6790. if (t.type === TOKEN_START_TAG && IN_HEAD_NOSCRIPT_PASSTHROUGH.has(t.name)) {
  6791. return modes.inHead(t);
  6792. }
  6793. // A stray end tag other than </br>/</noscript> is ignored (the comment
  6794. // or content stays inside <noscript>); only </br> and other content fall
  6795. // back to popping <noscript>.
  6796. if (t.type === TOKEN_END_TAG && t.name !== "br") return;
  6797. if (
  6798. t.type === TOKEN_START_TAG &&
  6799. (t.name === "head" || t.name === "noscript")
  6800. ) {
  6801. return;
  6802. }
  6803. popOpen();
  6804. mode = MODE_IN_HEAD;
  6805. process(t);
  6806. };
  6807. modes.afterHead = (t) => {
  6808. if (t.type === TOKEN_CHAR) {
  6809. const r = leadingWs(t, true);
  6810. if (!r) return;
  6811. insertHtmlElement("body", EMPTY_ATTRS, null);
  6812. mode = MODE_IN_BODY;
  6813. process(r);
  6814. return;
  6815. }
  6816. if (isCommentOrProcessingInstruction(t)) {
  6817. insertCommentOrProcessingInstruction(t);
  6818. return;
  6819. }
  6820. if (t.type === TOKEN_DOCTYPE) return;
  6821. if (t.type === TOKEN_START_TAG) {
  6822. if (t.name === "html") return modes.inBody(t);
  6823. if (t.name === "body") {
  6824. insertHtmlElement("body", t.attrs, t.pos);
  6825. framesetOk = false;
  6826. mode = MODE_IN_BODY;
  6827. return;
  6828. }
  6829. if (t.name === "frameset") {
  6830. insertHtmlElement("frameset", t.attrs, t.pos);
  6831. mode = MODE_IN_FRAMESET;
  6832. return;
  6833. }
  6834. if (HEAD_ELEMENTS.has(t.name)) {
  6835. const headEl = /** @type {HtmlElement} */ (head);
  6836. open.push(headEl);
  6837. modes.inHead(t);
  6838. const idx = open.indexOf(headEl);
  6839. if (idx !== -1) removeOpenAt(idx);
  6840. return;
  6841. }
  6842. if (t.name === "head") return;
  6843. }
  6844. if (t.type === TOKEN_END_TAG) {
  6845. if (t.name === "template") return modes.inHead(t);
  6846. if (!BODY_HTML_BR.has(t.name)) return;
  6847. }
  6848. insertHtmlElement("body", EMPTY_ATTRS, null);
  6849. mode = MODE_IN_BODY;
  6850. process(t);
  6851. };
  6852. modes.inBody = (t) => {
  6853. if (t.type === TOKEN_CHAR) {
  6854. // Deliberately not guarded by `inputHasNul`: this `includes` flattens the
  6855. // decoded rope, and skipping it makes the inserts below allocate more.
  6856. if (t.data.includes("\0")) t = { ...t, data: t.data.replace(/\0/g, "") };
  6857. if (t.data === "") return;
  6858. reconstructAfe();
  6859. insertCharacters(t.data, t.start, t.end);
  6860. // `framesetOk` only ever goes true→false, so once it's false skip the
  6861. // per-text-token whitespace scan entirely (it flips false very early in
  6862. // real documents).
  6863. if (framesetOk && !isAllWs(t.data)) framesetOk = false;
  6864. return;
  6865. }
  6866. if (isCommentOrProcessingInstruction(t)) {
  6867. insertCommentOrProcessingInstruction(t);
  6868. return;
  6869. }
  6870. if (t.type === TOKEN_DOCTYPE) return;
  6871. if (t.type === TOKEN_START_TAG) return startTagInBody(t);
  6872. if (t.type === TOKEN_END_TAG) return endTagInBody(t);
  6873. if (t.type === TOKEN_EOF && templateModes.length) {
  6874. return modes.inTemplate(t);
  6875. }
  6876. };
  6877. const closeIfPInButtonScope = () => {
  6878. if (inButtonScope("p")) closePElement();
  6879. };
  6880. // "any other end tag" in body: pop to the matching open element, stopping at
  6881. // the first special element; also the adoption agency's no-element fallback.
  6882. const anyOtherEndTag = (/** @type {string} */ name) => {
  6883. for (let i = open.length - 1; i >= 0; i--) {
  6884. const node = open[i];
  6885. if (_namespaceOf(node) === NS_HTML && _tagNameOf(node) === name) {
  6886. generateImpliedEndTags(name);
  6887. while (open.length > i) {
  6888. _nodeEnds[open[open.length - 1]] = tokenEnd;
  6889. popOpen();
  6890. }
  6891. return;
  6892. }
  6893. if (isSpecial(node)) return;
  6894. }
  6895. };
  6896. const startTagInBody = (/** @type {StartTagToken} */ t) => {
  6897. const name = t.name;
  6898. if (name === "html") {
  6899. if (open.some(isHtmlTemplateEl)) {
  6900. return;
  6901. }
  6902. mergeAttrs(open[0], t.attrs);
  6903. return;
  6904. }
  6905. if (HEAD_ELEMENTS.has(name)) {
  6906. return modes.inHead(t);
  6907. }
  6908. if (name === "body") {
  6909. const second = open[1];
  6910. if (
  6911. !second ||
  6912. _tagNameOf(second) !== "body" ||
  6913. open.some(isHtmlTemplateEl)
  6914. ) {
  6915. return;
  6916. }
  6917. framesetOk = false;
  6918. mergeAttrs(second, t.attrs);
  6919. return;
  6920. }
  6921. if (name === "frameset") {
  6922. const second = open[1];
  6923. if (!second || _tagNameOf(second) !== "body") return;
  6924. if (!framesetOk) return;
  6925. detach(second);
  6926. while (open.length > 1) popOpen();
  6927. insertHtmlElement("frameset", t.attrs, t.pos);
  6928. mode = MODE_IN_FRAMESET;
  6929. return;
  6930. }
  6931. if (BLOCK_START.has(name)) {
  6932. closeIfPInButtonScope();
  6933. insertHtmlElement(name, t.attrs, t.pos);
  6934. return;
  6935. }
  6936. if (HEADING.has(name)) {
  6937. closeIfPInButtonScope();
  6938. if (_namespaceOf(cur()) === NS_HTML && HEADING.has(_tagNameOf(cur()))) {
  6939. popOpen();
  6940. }
  6941. insertHtmlElement(name, t.attrs, t.pos);
  6942. return;
  6943. }
  6944. if (name === "pre" || name === "listing") {
  6945. closeIfPInButtonScope();
  6946. insertHtmlElement(name, t.attrs, t.pos);
  6947. t.swallowNewline = true;
  6948. framesetOk = false;
  6949. return;
  6950. }
  6951. if (name === "form") {
  6952. if (form && !open.some(isHtmlTemplateEl)) {
  6953. return;
  6954. }
  6955. closeIfPInButtonScope();
  6956. // The end tag this needs generates implied end tags where the source's did
  6957. // not, so anything open between the two forms moves.
  6958. if (_formResetWouldPop()) _printFromSource = true;
  6959. const el = insertHtmlElement("form", t.attrs, t.pos);
  6960. if (!open.some(isHtmlTemplateEl)) {
  6961. form = el;
  6962. }
  6963. return;
  6964. }
  6965. if (name === "li") {
  6966. framesetOk = false;
  6967. for (let i = open.length - 1; i >= 0; i--) {
  6968. const node = open[i];
  6969. if (_namespaceOf(node) === NS_HTML && _tagNameOf(node) === "li") {
  6970. generateImpliedEndTags("li");
  6971. popUntil("li");
  6972. break;
  6973. }
  6974. if (
  6975. isSpecial(node) &&
  6976. !(_namespaceOf(node) === NS_HTML && ADDRESS_DIV_P.has(_tagNameOf(node)))
  6977. ) {
  6978. // Inlined rather than a call: this runs for every `<li>` in a list.
  6979. for (let j = i - 1; j >= 0 && !_printFromSource; j--) {
  6980. const below = open[j];
  6981. if (_namespaceOf(below) === NS_HTML && _tagNameOf(below) === "li") {
  6982. _printFromSource = true;
  6983. }
  6984. }
  6985. break;
  6986. }
  6987. }
  6988. closeIfPInButtonScope();
  6989. insertHtmlElement("li", t.attrs, t.pos);
  6990. return;
  6991. }
  6992. if (name === "dd" || name === "dt") {
  6993. framesetOk = false;
  6994. for (let i = open.length - 1; i >= 0; i--) {
  6995. const node = open[i];
  6996. if (
  6997. _namespaceOf(node) === NS_HTML &&
  6998. (_tagNameOf(node) === "dd" || _tagNameOf(node) === "dt")
  6999. ) {
  7000. generateImpliedEndTags(_tagNameOf(node));
  7001. popUntil(_tagNameOf(node));
  7002. break;
  7003. }
  7004. if (
  7005. isSpecial(node) &&
  7006. !(_namespaceOf(node) === NS_HTML && ADDRESS_DIV_P.has(_tagNameOf(node)))
  7007. ) {
  7008. for (let j = i - 1; j >= 0 && !_printFromSource; j--) {
  7009. const below = open[j];
  7010. if (_namespaceOf(below) !== NS_HTML) continue;
  7011. const bn = _tagNameOf(below);
  7012. if (bn === "dd" || bn === "dt") _printFromSource = true;
  7013. }
  7014. break;
  7015. }
  7016. }
  7017. closeIfPInButtonScope();
  7018. insertHtmlElement(name, t.attrs, t.pos);
  7019. return;
  7020. }
  7021. if (name === "plaintext") {
  7022. closeIfPInButtonScope();
  7023. insertHtmlElement("plaintext", t.attrs, t.pos);
  7024. return;
  7025. }
  7026. if (name === "button") {
  7027. if (inScope("button")) {
  7028. generateImpliedEndTags();
  7029. popUntil("button");
  7030. }
  7031. reconstructAfe();
  7032. insertHtmlElement("button", t.attrs, t.pos);
  7033. framesetOk = false;
  7034. return;
  7035. }
  7036. if (name === "a") {
  7037. // if there's an <a> in afe after last marker
  7038. for (let i = afe.length - 1; i >= 0; i--) {
  7039. if (afe[i] === AFE_MARKER) {
  7040. if (_afeNameBelowMarker(i, "a")) _printFromSource = true;
  7041. break;
  7042. }
  7043. if (_tagNameOf(afe[i]) === "a") {
  7044. adoptionAgency("a", t.pos);
  7045. const idx = afe.findIndex(
  7046. (e) => e !== AFE_MARKER && _tagNameOf(e) === "a"
  7047. );
  7048. if (idx !== -1) {
  7049. const el = afe[idx];
  7050. afeRemoveAt(idx);
  7051. const oi = open.indexOf(el);
  7052. if (oi !== -1) removeOpenAt(oi);
  7053. }
  7054. break;
  7055. }
  7056. }
  7057. reconstructAfe();
  7058. const el = insertHtmlElement("a", t.attrs, t.pos);
  7059. pushAfe(el);
  7060. return;
  7061. }
  7062. if (FORMATTING.has(name) && name !== "a" && name !== "nobr") {
  7063. reconstructAfe();
  7064. const el = insertHtmlElement(name, t.attrs, t.pos);
  7065. pushAfe(el);
  7066. return;
  7067. }
  7068. if (name === "nobr") {
  7069. reconstructAfe();
  7070. if (inScope("nobr")) {
  7071. // The adoption agency returns false when a marker shields the nobr
  7072. // from the active formatting list; then act as "any other end tag".
  7073. if (!adoptionAgency("nobr", t.pos)) anyOtherEndTag("nobr");
  7074. reconstructAfe();
  7075. }
  7076. const el = insertHtmlElement("nobr", t.attrs, t.pos);
  7077. pushAfe(el);
  7078. return;
  7079. }
  7080. if (APPLET_MARQUEE_OBJECT.has(name)) {
  7081. reconstructAfe();
  7082. insertHtmlElement(name, t.attrs, t.pos);
  7083. insertMarker();
  7084. framesetOk = false;
  7085. return;
  7086. }
  7087. if (name === "table") {
  7088. if (!quirks) closeIfPInButtonScope();
  7089. insertHtmlElement("table", t.attrs, t.pos);
  7090. framesetOk = false;
  7091. mode = MODE_IN_TABLE;
  7092. return;
  7093. }
  7094. if (VOID_FORMATTING.has(name)) {
  7095. reconstructAfe();
  7096. insertHtmlElement(name, t.attrs, t.pos);
  7097. popOpen();
  7098. framesetOk = false;
  7099. return;
  7100. }
  7101. if (name === "input") {
  7102. // `<input>` inside a select is dropped; if a select is open it is
  7103. // closed first (keygen/textarea no longer behave this way).
  7104. if (inScope("select")) {
  7105. popUntil("select");
  7106. resetInsertionMode();
  7107. } else if (
  7108. fragment &&
  7109. _namespaceOf(fragment) === NS_HTML &&
  7110. _tagNameOf(fragment) === "select"
  7111. ) {
  7112. return;
  7113. }
  7114. reconstructAfe();
  7115. insertHtmlElement("input", t.attrs, t.pos);
  7116. popOpen();
  7117. const ty = _findAttr(t.attrs.start, t.attrs.count, "type");
  7118. if (ty === 0 || _attrValueOf(ty).toLowerCase() !== "hidden") {
  7119. framesetOk = false;
  7120. }
  7121. return;
  7122. }
  7123. if (PARAM_SOURCE_TRACK.has(name)) {
  7124. insertHtmlElement(name, t.attrs, t.pos);
  7125. popOpen();
  7126. return;
  7127. }
  7128. if (name === "hr") {
  7129. closeIfPInButtonScope();
  7130. // An `<hr>` is a `<select>`'s own separator, so inside one it leaves what
  7131. // it was written in; outside one it is ordinary content and nests.
  7132. if (inScope("select")) generateImpliedEndTags();
  7133. insertHtmlElement("hr", t.attrs, t.pos);
  7134. popOpen();
  7135. framesetOk = false;
  7136. return;
  7137. }
  7138. if (name === "image") {
  7139. return startTagInBody({ ...t, name: "img" });
  7140. }
  7141. if (name === "textarea") {
  7142. genericRcdata(t, true);
  7143. framesetOk = false;
  7144. return;
  7145. }
  7146. if (name === "xmp") {
  7147. closeIfPInButtonScope();
  7148. reconstructAfe();
  7149. framesetOk = false;
  7150. genericRawtext(t);
  7151. return;
  7152. }
  7153. if (name === "iframe") {
  7154. framesetOk = false;
  7155. genericRawtext(t);
  7156. return;
  7157. }
  7158. if (name === "noembed") {
  7159. genericRawtext(t);
  7160. return;
  7161. }
  7162. if (name === "select") {
  7163. reconstructAfe();
  7164. if (inScope("select")) {
  7165. generateImpliedEndTags();
  7166. popUntil("select");
  7167. resetInsertionMode();
  7168. return;
  7169. }
  7170. insertHtmlElement("select", t.attrs, t.pos);
  7171. framesetOk = false;
  7172. return;
  7173. }
  7174. if (name === "optgroup" || name === "option") {
  7175. // Inside a select these end what is open like a list item does, an
  7176. // `<option>` sparing its `<optgroup>`; outside one only an option gives way.
  7177. if (inScope("select")) {
  7178. generateImpliedEndTags(name === "option" ? "optgroup" : "");
  7179. } else if (
  7180. _namespaceOf(cur()) === NS_HTML &&
  7181. _tagNameOf(cur()) === "option"
  7182. ) {
  7183. popOpen();
  7184. }
  7185. reconstructAfe();
  7186. insertHtmlElement(name, t.attrs, t.pos);
  7187. return;
  7188. }
  7189. if (name === "rb" || name === "rtc") {
  7190. if (inScope("ruby")) generateImpliedEndTags();
  7191. insertHtmlElement(name, t.attrs, t.pos);
  7192. return;
  7193. }
  7194. if (name === "rp" || name === "rt") {
  7195. if (inScope("ruby")) generateImpliedEndTags("rtc");
  7196. insertHtmlElement(name, t.attrs, t.pos);
  7197. return;
  7198. }
  7199. if (name === "math") {
  7200. reconstructAfe();
  7201. const attrs = adjustForeignAttrs(adjustMathmlAttrs(t.attrs), NS_MATHML);
  7202. insertForeignElement("math", NS_MATHML, attrs, t.pos);
  7203. if (t.selfClosing) popOpen();
  7204. return;
  7205. }
  7206. if (name === "svg") {
  7207. reconstructAfe();
  7208. const attrs = adjustForeignAttrs(t.attrs, NS_SVG);
  7209. insertForeignElement("svg", NS_SVG, attrs, t.pos);
  7210. if (t.selfClosing) popOpen();
  7211. return;
  7212. }
  7213. if (IGNORED_BODY_TABLE_STARTS.has(name)) {
  7214. return;
  7215. }
  7216. // any other start tag
  7217. reconstructAfe();
  7218. insertHtmlElement(name, t.attrs, t.pos);
  7219. };
  7220. const endTagInBody = (/** @type {EndTagToken} */ t) => {
  7221. const name = t.name;
  7222. if (name === "template") return modes.inHead(t);
  7223. if (name === "select") {
  7224. if (!inScope("select")) return;
  7225. generateImpliedEndTags();
  7226. popUntil("select");
  7227. return;
  7228. }
  7229. if (name === "body" || name === "html") {
  7230. if (!inScope("body")) return;
  7231. mode = MODE_AFTER_BODY;
  7232. if (name === "html") process(t);
  7233. return;
  7234. }
  7235. if (BLOCK_END.has(name)) {
  7236. if (!inScope(name)) return;
  7237. generateImpliedEndTags();
  7238. popUntil(name);
  7239. return;
  7240. }
  7241. if (name === "form") {
  7242. if (!open.some(isHtmlTemplateEl)) {
  7243. const node = form;
  7244. form = 0;
  7245. if (!node || !inScopeEl(node)) return;
  7246. generateImpliedEndTags();
  7247. const idx = open.indexOf(node);
  7248. if (idx !== -1) removeOpenAt(idx);
  7249. } else {
  7250. if (!inScope("form")) return;
  7251. generateImpliedEndTags();
  7252. popUntil("form");
  7253. }
  7254. return;
  7255. }
  7256. if (name === "p") {
  7257. if (!inButtonScope("p")) insertHtmlElement("p", EMPTY_ATTRS, t.pos);
  7258. closePElement();
  7259. return;
  7260. }
  7261. if (name === "li") {
  7262. if (!inListItemScope("li")) return;
  7263. generateImpliedEndTags("li");
  7264. popUntil("li");
  7265. return;
  7266. }
  7267. if (name === "dd" || name === "dt") {
  7268. if (!inScope(name)) return;
  7269. generateImpliedEndTags(name);
  7270. popUntil(name);
  7271. return;
  7272. }
  7273. if (HEADING.has(name)) {
  7274. let anyHeadingInScope = false;
  7275. for (const h of HEADING) {
  7276. if (inScope(h)) {
  7277. anyHeadingInScope = true;
  7278. break;
  7279. }
  7280. }
  7281. if (!anyHeadingInScope) return;
  7282. generateImpliedEndTags();
  7283. popUntilOneOf(HEADING);
  7284. return;
  7285. }
  7286. if (name === "sarcasm") {
  7287. /* take a deep breath */
  7288. }
  7289. if (FORMATTING.has(name)) {
  7290. // With no such element in the list, the algorithm hands the token back
  7291. // (§13.2.6.4.7, "any other end tag") rather than swallowing it — which is
  7292. // what Noah's Ark leaves behind: an element still open that the list no
  7293. // longer names.
  7294. if (!adoptionAgency(name, t.pos)) anyOtherEndTag(name);
  7295. return;
  7296. }
  7297. if (APPLET_MARQUEE_OBJECT.has(name)) {
  7298. if (!inScope(name)) return;
  7299. generateImpliedEndTags();
  7300. popUntil(name);
  7301. clearAfeToMarker();
  7302. return;
  7303. }
  7304. if (name === "br") {
  7305. reconstructAfe();
  7306. insertHtmlElement("br", EMPTY_ATTRS, t.pos);
  7307. popOpen();
  7308. framesetOk = false;
  7309. return;
  7310. }
  7311. anyOtherEndTag(name);
  7312. };
  7313. // generic RCDATA/RAWTEXT: tokenizer already emits the text + end tag, so we
  7314. // just insert the element and switch to "text" mode; text mode appends chars
  7315. // and the matching end tag pops.
  7316. const genericRawtext = (/** @type {StartTagToken} */ t) => {
  7317. insertHtmlElement(t.name, t.attrs, t.pos);
  7318. originalMode = mode;
  7319. mode = MODE_TEXT;
  7320. };
  7321. const genericRcdata = (/** @type {StartTagToken} */ t, swallow = false) => {
  7322. insertHtmlElement(t.name, t.attrs, t.pos);
  7323. if (swallow) t.swallowNewline = true;
  7324. originalMode = mode;
  7325. mode = MODE_TEXT;
  7326. };
  7327. modes.text = (t) => {
  7328. if (t.type === TOKEN_CHAR) {
  7329. insertCharacters(t.data, t.start, t.end);
  7330. return;
  7331. }
  7332. if (t.type === TOKEN_EOF) {
  7333. if (open.length) popOpen();
  7334. mode = originalMode;
  7335. process(t);
  7336. return;
  7337. }
  7338. if (t.type === TOKEN_END_TAG) {
  7339. // Treat as rawtext rather than an end tag when it can't close the
  7340. // current element: a non-matching name (e.g. a fragment context), or
  7341. // a name that ran straight to EOF with no delimiter (`</script` at
  7342. // EOF — the tokenizer still emits a partial tag there).
  7343. if (
  7344. cur() &&
  7345. (t.name !== _tagNameOf(cur()) || t.pos.end === t.pos.nameEnd)
  7346. ) {
  7347. insertCharacters(
  7348. source.slice(t.pos.start, t.pos.end),
  7349. t.pos.start,
  7350. t.pos.end
  7351. );
  7352. return;
  7353. }
  7354. // span the close tag, like every other explicit-close pop
  7355. _nodeEnds[/** @type {HtmlElement} */ (popOpen())] = tokenEnd;
  7356. mode = originalMode;
  7357. }
  7358. };
  7359. // ---------- table modes ----------
  7360. /** @type {{ list: CharToken[], hasNonWs: boolean } | null} */
  7361. let pendingTableChars = null;
  7362. modes.inTable = (t) => {
  7363. if (t.type === TOKEN_CHAR) {
  7364. const c = cur();
  7365. if (TABLE_CONTEXT.has(_tagNameOf(c)) && _namespaceOf(c) === NS_HTML) {
  7366. pendingTableChars = { list: [], hasNonWs: false };
  7367. originalMode = mode;
  7368. mode = MODE_IN_TABLE_TEXT;
  7369. return process(t);
  7370. }
  7371. }
  7372. if (isCommentOrProcessingInstruction(t)) {
  7373. insertCommentOrProcessingInstruction(t);
  7374. return;
  7375. }
  7376. if (t.type === TOKEN_DOCTYPE) return;
  7377. if (t.type === TOKEN_START_TAG) {
  7378. const name = t.name;
  7379. if (name === "caption") {
  7380. clearStackToTableContext();
  7381. insertMarker();
  7382. insertHtmlElement("caption", t.attrs, t.pos);
  7383. mode = MODE_IN_CAPTION;
  7384. return;
  7385. }
  7386. if (name === "colgroup") {
  7387. clearStackToTableContext();
  7388. insertHtmlElement("colgroup", t.attrs, t.pos);
  7389. mode = MODE_IN_COLUMN_GROUP;
  7390. return;
  7391. }
  7392. if (name === "col") {
  7393. clearStackToTableContext();
  7394. insertHtmlElement("colgroup", EMPTY_ATTRS, t.pos);
  7395. mode = MODE_IN_COLUMN_GROUP;
  7396. return process(t);
  7397. }
  7398. if (TBODY_GROUP.has(name)) {
  7399. clearStackToTableContext();
  7400. insertHtmlElement(name, t.attrs, t.pos);
  7401. mode = MODE_IN_TABLE_BODY;
  7402. return;
  7403. }
  7404. if (TD_TH_TR.has(name)) {
  7405. clearStackToTableContext();
  7406. insertHtmlElement("tbody", EMPTY_ATTRS, t.pos);
  7407. mode = MODE_IN_TABLE_BODY;
  7408. return process(t);
  7409. }
  7410. if (name === "table") {
  7411. if (!inTableScope("table")) return;
  7412. popUntil("table");
  7413. resetInsertionMode();
  7414. return process(t);
  7415. }
  7416. if (STYLE_SCRIPT_TEMPLATE.has(name)) {
  7417. return modes.inHead(t);
  7418. }
  7419. if (name === "input") {
  7420. const ty = _findAttr(t.attrs.start, t.attrs.count, "type");
  7421. if (ty !== 0 && _attrValueOf(ty).toLowerCase() === "hidden") {
  7422. insertHtmlElement("input", t.attrs, t.pos);
  7423. popOpen();
  7424. return;
  7425. }
  7426. }
  7427. if (name === "form") {
  7428. if (form || open.some(isHtmlTemplateEl)) {
  7429. return;
  7430. }
  7431. form = insertHtmlElement("form", t.attrs, t.pos);
  7432. popOpen();
  7433. return;
  7434. }
  7435. }
  7436. if (t.type === TOKEN_END_TAG) {
  7437. if (t.name === "table") {
  7438. if (!inTableScope("table")) return;
  7439. popUntil("table");
  7440. resetInsertionMode();
  7441. return;
  7442. }
  7443. if (IN_TABLE_IGNORED_ENDS.has(t.name)) {
  7444. return;
  7445. }
  7446. if (t.name === "template") return modes.inHead(t);
  7447. }
  7448. if (t.type === TOKEN_EOF) return modes.inBody(t);
  7449. // anything else: foster parenting
  7450. fosterParenting = true;
  7451. modes.inBody(t);
  7452. fosterParenting = false;
  7453. };
  7454. modes.inTableText = (t) => {
  7455. if (t.type === TOKEN_CHAR) {
  7456. const data =
  7457. inputHasNul && t.data.includes("\0") ? t.data.replace(/\0/g, "") : t.data;
  7458. if (data === "") return;
  7459. // Snapshot into a fresh token: these are buffered and replayed after
  7460. // later tokens arrive, so they must not alias the reused token.
  7461. /** @type {CharToken} */
  7462. const tc = { type: TOKEN_CHAR, data, start: t.start, end: t.end };
  7463. const pending = /** @type {{ list: CharToken[], hasNonWs: boolean }} */ (
  7464. pendingTableChars
  7465. );
  7466. pending.list.push(tc);
  7467. if (!isAllWs(tc.data)) pending.hasNonWs = true;
  7468. return;
  7469. }
  7470. // flush
  7471. const chars = /** @type {{ list: CharToken[], hasNonWs: boolean }} */ (
  7472. pendingTableChars
  7473. );
  7474. pendingTableChars = null;
  7475. mode = originalMode;
  7476. for (const ct of chars.list) {
  7477. if (chars.hasNonWs) {
  7478. fosterParenting = true;
  7479. modes.inBody(ct);
  7480. fosterParenting = false;
  7481. } else {
  7482. insertCharacters(ct.data, ct.start, ct.end);
  7483. }
  7484. }
  7485. process(t);
  7486. };
  7487. const clearStackToTableContext = () => {
  7488. while (open.length) {
  7489. const c = cur();
  7490. if (_namespaceOf(c) === NS_HTML && CLEAR_TABLE.has(_tagNameOf(c))) {
  7491. break;
  7492. }
  7493. popOpen();
  7494. }
  7495. };
  7496. const clearStackToTableBodyContext = () => {
  7497. while (open.length) {
  7498. const c = cur();
  7499. if (_namespaceOf(c) === NS_HTML && CLEAR_TABLE_BODY.has(_tagNameOf(c))) {
  7500. break;
  7501. }
  7502. popOpen();
  7503. }
  7504. };
  7505. const clearStackToTableRowContext = () => {
  7506. while (open.length) {
  7507. const c = cur();
  7508. if (_namespaceOf(c) === NS_HTML && CLEAR_TABLE_ROW.has(_tagNameOf(c))) {
  7509. break;
  7510. }
  7511. popOpen();
  7512. }
  7513. };
  7514. modes.inCaption = (t) => {
  7515. if (
  7516. (t.type === TOKEN_END_TAG && t.name === "caption") ||
  7517. (t.type === TOKEN_START_TAG && CAPTION_TABLE_STARTS.has(t.name)) ||
  7518. (t.type === TOKEN_END_TAG && t.name === "table")
  7519. ) {
  7520. if (!inTableScope("caption")) return;
  7521. generateImpliedEndTags();
  7522. popUntil("caption");
  7523. clearAfeToMarker();
  7524. mode = MODE_IN_TABLE;
  7525. if (!(t.type === TOKEN_END_TAG && t.name === "caption")) {
  7526. return process(t);
  7527. }
  7528. return;
  7529. }
  7530. if (t.type === TOKEN_END_TAG && CAPTION_IGNORED_ENDS.has(t.name)) {
  7531. return;
  7532. }
  7533. return modes.inBody(t);
  7534. };
  7535. modes.inColumnGroup = (t) => {
  7536. if (t.type === TOKEN_CHAR) {
  7537. const r = leadingWs(t, true);
  7538. if (!r) return;
  7539. if (_tagNameOf(cur()) !== "colgroup") return;
  7540. popOpen();
  7541. mode = MODE_IN_TABLE;
  7542. process(r);
  7543. return;
  7544. }
  7545. if (isCommentOrProcessingInstruction(t)) {
  7546. insertCommentOrProcessingInstruction(t);
  7547. return;
  7548. }
  7549. if (t.type === TOKEN_DOCTYPE) return;
  7550. if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t);
  7551. if (t.type === TOKEN_START_TAG && t.name === "col") {
  7552. insertHtmlElement("col", t.attrs, t.pos);
  7553. popOpen();
  7554. return;
  7555. }
  7556. if (t.type === TOKEN_END_TAG && t.name === "colgroup") {
  7557. if (_tagNameOf(cur()) !== "colgroup") return;
  7558. popOpen();
  7559. mode = MODE_IN_TABLE;
  7560. return;
  7561. }
  7562. if (t.type === TOKEN_END_TAG && t.name === "col") return;
  7563. if (
  7564. (t.type === TOKEN_START_TAG || t.type === TOKEN_END_TAG) &&
  7565. t.name === "template"
  7566. ) {
  7567. return modes.inHead(t);
  7568. }
  7569. if (t.type === TOKEN_EOF) return modes.inBody(t);
  7570. if (_tagNameOf(cur()) !== "colgroup") return;
  7571. popOpen();
  7572. mode = MODE_IN_TABLE;
  7573. process(t);
  7574. };
  7575. modes.inTableBody = (t) => {
  7576. if (t.type === TOKEN_START_TAG && t.name === "tr") {
  7577. clearStackToTableBodyContext();
  7578. insertHtmlElement("tr", t.attrs, t.pos);
  7579. mode = MODE_IN_ROW;
  7580. return;
  7581. }
  7582. if (t.type === TOKEN_START_TAG && (t.name === "th" || t.name === "td")) {
  7583. clearStackToTableBodyContext();
  7584. insertHtmlElement("tr", EMPTY_ATTRS, t.pos);
  7585. mode = MODE_IN_ROW;
  7586. return process(t);
  7587. }
  7588. if (t.type === TOKEN_END_TAG && TBODY_GROUP.has(t.name)) {
  7589. if (!inTableScope(t.name)) return;
  7590. clearStackToTableBodyContext();
  7591. popOpen();
  7592. mode = MODE_IN_TABLE;
  7593. return;
  7594. }
  7595. if (
  7596. (t.type === TOKEN_START_TAG && TBODY_TRIGGER_STARTS.has(t.name)) ||
  7597. (t.type === TOKEN_END_TAG && t.name === "table")
  7598. ) {
  7599. if (!inTableScope(TBODY_GROUP)) return;
  7600. clearStackToTableBodyContext();
  7601. popOpen();
  7602. mode = MODE_IN_TABLE;
  7603. return process(t);
  7604. }
  7605. if (t.type === TOKEN_END_TAG && TBODY_IGNORED_ENDS.has(t.name)) {
  7606. return;
  7607. }
  7608. return modes.inTable(t);
  7609. };
  7610. modes.inRow = (t) => {
  7611. if (t.type === TOKEN_START_TAG && (t.name === "th" || t.name === "td")) {
  7612. clearStackToTableRowContext();
  7613. insertHtmlElement(t.name, t.attrs, t.pos);
  7614. mode = MODE_IN_CELL;
  7615. insertMarker();
  7616. return;
  7617. }
  7618. if (t.type === TOKEN_END_TAG && t.name === "tr") {
  7619. if (!inTableScope("tr")) return;
  7620. clearStackToTableRowContext();
  7621. popOpen();
  7622. mode = MODE_IN_TABLE_BODY;
  7623. return;
  7624. }
  7625. if (
  7626. (t.type === TOKEN_START_TAG && ROW_TRIGGER_STARTS.has(t.name)) ||
  7627. (t.type === TOKEN_END_TAG && t.name === "table")
  7628. ) {
  7629. if (!inTableScope("tr")) return;
  7630. clearStackToTableRowContext();
  7631. popOpen();
  7632. mode = MODE_IN_TABLE_BODY;
  7633. return process(t);
  7634. }
  7635. if (t.type === TOKEN_END_TAG && TBODY_GROUP.has(t.name)) {
  7636. if (!inTableScope(t.name)) return;
  7637. if (!inTableScope("tr")) return;
  7638. clearStackToTableRowContext();
  7639. popOpen();
  7640. mode = MODE_IN_TABLE_BODY;
  7641. return process(t);
  7642. }
  7643. if (t.type === TOKEN_END_TAG && ROW_IGNORED_ENDS.has(t.name)) {
  7644. return;
  7645. }
  7646. return modes.inTable(t);
  7647. };
  7648. modes.inCell = (t) => {
  7649. if (t.type === TOKEN_END_TAG && (t.name === "td" || t.name === "th")) {
  7650. if (!inTableScope(t.name)) return;
  7651. generateImpliedEndTags();
  7652. popUntil(t.name);
  7653. clearAfeToMarker();
  7654. mode = MODE_IN_ROW;
  7655. return;
  7656. }
  7657. if (t.type === TOKEN_START_TAG && CAPTION_TABLE_STARTS.has(t.name)) {
  7658. if (!inTableScope("td") && !inTableScope("th")) return;
  7659. closeCell();
  7660. return process(t);
  7661. }
  7662. if (t.type === TOKEN_END_TAG && TABLE_CONTEXT.has(t.name)) {
  7663. if (!inTableScope(t.name)) return;
  7664. closeCell();
  7665. return process(t);
  7666. }
  7667. if (t.type === TOKEN_END_TAG && CELL_IGNORED_ENDS.has(t.name)) {
  7668. return;
  7669. }
  7670. return modes.inBody(t);
  7671. };
  7672. const closeCell = () => {
  7673. generateImpliedEndTags();
  7674. popUntilOneOf(TD_TH);
  7675. clearAfeToMarker();
  7676. mode = MODE_IN_ROW;
  7677. };
  7678. modes.inTemplate = (t) => {
  7679. if (
  7680. t.type === TOKEN_CHAR ||
  7681. isCommentOrProcessingInstruction(t) ||
  7682. t.type === TOKEN_DOCTYPE
  7683. ) {
  7684. return modes.inBody(t);
  7685. }
  7686. if (t.type === TOKEN_START_TAG) {
  7687. if (HEAD_ELEMENTS.has(t.name)) {
  7688. return modes.inHead(t);
  7689. }
  7690. const target = TEMPLATE_START_TAG_MODES.get(t.name) || MODE_IN_BODY;
  7691. templateModes[templateModes.length - 1] = target;
  7692. mode = target;
  7693. return process(t);
  7694. }
  7695. if (t.type === TOKEN_END_TAG) {
  7696. if (t.name === "template") return modes.inHead(t);
  7697. return;
  7698. }
  7699. if (t.type === TOKEN_EOF) {
  7700. if (!open.some(isHtmlTemplateEl)) {
  7701. return;
  7702. }
  7703. popUntil("template");
  7704. clearAfeToMarker();
  7705. templateModes.pop();
  7706. resetInsertionMode();
  7707. return process(t);
  7708. }
  7709. };
  7710. modes.afterBody = (t) => {
  7711. if (t.type === TOKEN_CHAR && isAllWs(t.data)) return modes.inBody(t);
  7712. if (isCommentOrProcessingInstruction(t)) {
  7713. insertCommentOrProcessingInstruction(t, {
  7714. parent: open[0],
  7715. beforeNode: 0
  7716. });
  7717. return;
  7718. }
  7719. if (t.type === TOKEN_DOCTYPE) return;
  7720. if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t);
  7721. if (t.type === TOKEN_END_TAG && t.name === "html") {
  7722. if (fragment) return;
  7723. mode = MODE_AFTER_AFTER_BODY;
  7724. return;
  7725. }
  7726. if (t.type === TOKEN_EOF) return;
  7727. mode = MODE_IN_BODY;
  7728. process(t);
  7729. };
  7730. modes.inFrameset = (t) => {
  7731. if (t.type === TOKEN_CHAR) {
  7732. const ws = t.data.replace(/[^\t\n\f\r ]/g, "");
  7733. if (ws) insertCharacters(ws, t.start, t.end);
  7734. return;
  7735. }
  7736. if (isCommentOrProcessingInstruction(t)) {
  7737. insertCommentOrProcessingInstruction(t);
  7738. return;
  7739. }
  7740. if (t.type === TOKEN_DOCTYPE) return;
  7741. if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t);
  7742. if (t.type === TOKEN_START_TAG && t.name === "frameset") {
  7743. insertHtmlElement("frameset", t.attrs, t.pos);
  7744. return;
  7745. }
  7746. if (t.type === TOKEN_END_TAG && t.name === "frameset") {
  7747. if (_tagNameOf(cur()) === "html") return;
  7748. popOpen();
  7749. if (!fragment && _tagNameOf(cur()) !== "frameset") {
  7750. mode = MODE_AFTER_FRAMESET;
  7751. }
  7752. return;
  7753. }
  7754. if (t.type === TOKEN_START_TAG && t.name === "frame") {
  7755. insertHtmlElement("frame", t.attrs, t.pos);
  7756. popOpen();
  7757. return;
  7758. }
  7759. if (t.type === TOKEN_START_TAG && t.name === "noframes") {
  7760. return modes.inHead(t);
  7761. }
  7762. };
  7763. modes.afterFrameset = (t) => {
  7764. if (t.type === TOKEN_CHAR) {
  7765. const ws = t.data.replace(/[^\t\n\f\r ]/g, "");
  7766. if (ws) insertCharacters(ws, t.start, t.end);
  7767. return;
  7768. }
  7769. if (isCommentOrProcessingInstruction(t)) {
  7770. insertCommentOrProcessingInstruction(t);
  7771. return;
  7772. }
  7773. if (t.type === TOKEN_DOCTYPE) return;
  7774. if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t);
  7775. if (t.type === TOKEN_END_TAG && t.name === "html") {
  7776. mode = MODE_AFTER_AFTER_FRAMESET;
  7777. return;
  7778. }
  7779. if (t.type === TOKEN_START_TAG && t.name === "noframes") {
  7780. return modes.inHead(t);
  7781. }
  7782. };
  7783. modes.afterAfterBody = (t) => {
  7784. if (isCommentOrProcessingInstruction(t)) {
  7785. insertCommentOrProcessingInstruction(t, { parent: doc, beforeNode: 0 });
  7786. return;
  7787. }
  7788. if (t.type === TOKEN_DOCTYPE) return modes.inBody(t);
  7789. if (t.type === TOKEN_CHAR && isAllWs(t.data)) return modes.inBody(t);
  7790. if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t);
  7791. if (t.type === TOKEN_EOF) return;
  7792. mode = MODE_IN_BODY;
  7793. process(t);
  7794. };
  7795. modes.afterAfterFrameset = (t) => {
  7796. if (isCommentOrProcessingInstruction(t)) {
  7797. insertCommentOrProcessingInstruction(t, { parent: doc, beforeNode: 0 });
  7798. return;
  7799. }
  7800. if (t.type === TOKEN_DOCTYPE) return modes.inBody(t);
  7801. if (t.type === TOKEN_CHAR && isAllWs(t.data)) return modes.inBody(t);
  7802. if (t.type === TOKEN_START_TAG && t.name === "html") return modes.inBody(t);
  7803. if (t.type === TOKEN_START_TAG && t.name === "noframes") {
  7804. return modes.inHead(t);
  7805. }
  7806. };
  7807. const dispatch = () => {
  7808. process(/** @type {Token} */ (/** @type {unknown} */ (tok)));
  7809. // One boolean per token: only a stack change can complete a subtree.
  7810. if (_streamEligible && (_openStackChanged || _openSpliced)) {
  7811. _openStackChanged = false;
  7812. if (_streaming) {
  7813. _streamSync();
  7814. } else if (_nodeCount >= _STREAM_MIN_NODES) {
  7815. // Big enough to be worth recycling: start streaming here. The tree so
  7816. // far is unvisited, so the first sync enters the open stack and flushes
  7817. // everything already finished under it.
  7818. _streaming = true;
  7819. _streamSync();
  7820. }
  7821. }
  7822. };
  7823. // Tokenizer callbacks, hoisted to module scope and closing over the module-
  7824. // scope parser state. `tokenize`'s indirect `callbacks.text(...)` / `openTag`
  7825. // / `attribute(...)` sites then see one stable function identity across every
  7826. // parse, staying monomorphic so V8 can inline them — a fresh per-parse closure
  7827. // made them megamorphic and blocked inlining. `parseHtml` only refreshes
  7828. // `fragmentContext` on the shared object before each tokenize.
  7829. /** @type {() => boolean} */
  7830. const currentNodeIsForeign = () => {
  7831. const adjustedCurrentNode = adjustedCurrent();
  7832. return (
  7833. open.length > 0 &&
  7834. adjustedCurrentNode !== 0 &&
  7835. _namespaceOf(adjustedCurrentNode) !== NS_HTML
  7836. );
  7837. };
  7838. /**
  7839. * Whether the tree builder skips the tokenizer switch for this start tag, so
  7840. * its content stays markup: foreign content, and every frameset-mode tag but
  7841. * `noframes`, which those modes ignore rather than hand to the head rules.
  7842. * @param {string} name the start tag's lowercased name
  7843. * @returns {boolean} true when the tokenizer stays in the data state
  7844. */
  7845. const vetoesContentMode = (name) => {
  7846. if (currentNodeIsForeign()) return true;
  7847. return (
  7848. name !== "noframes" &&
  7849. (mode === MODE_IN_FRAMESET ||
  7850. mode === MODE_AFTER_FRAMESET ||
  7851. mode === MODE_AFTER_AFTER_FRAMESET)
  7852. );
  7853. };
  7854. /** @type {(input: string, code: string) => void} */
  7855. const handleParseError = (input, code) => {
  7856. if (code === "eof-in-tag") eofInTag = true;
  7857. };
  7858. /**
  7859. * @param {string} input input
  7860. * @param {number} rangeStart start of the range
  7861. * @param {number} rangeEnd end of the range
  7862. * @returns {string} the range with NULLs replaced, per the DOCTYPE states
  7863. */
  7864. const doctypePart = (input, rangeStart, rangeEnd) => {
  7865. const raw = input.slice(rangeStart, rangeEnd);
  7866. return inputHasNul ? raw.replace(/\0/g, "\uFFFD") : raw;
  7867. };
  7868. /** @type {(input: string, start: number, end: number, nameStart: number, nameEnd: number, publicStart: number, publicEnd: number, systemStart: number, systemEnd: number, forceQuirks: boolean) => number} */
  7869. const handleDoctypeToken = (
  7870. input,
  7871. start,
  7872. end,
  7873. nameStart,
  7874. nameEnd,
  7875. publicStart,
  7876. publicEnd,
  7877. systemStart,
  7878. systemEnd,
  7879. forceQuirks
  7880. ) => {
  7881. // Only the token immediately after the start tag may be the swallowed
  7882. // newline; anything else clears the flag (§13.2.6.4.7 "the next token").
  7883. swallowNextNewline = false;
  7884. tok.type = TOKEN_DOCTYPE;
  7885. // A missing name and an empty one are the same to every consumer here: the
  7886. // spec's name states always append at least one character.
  7887. // ASCII-only folding: the name states lowercase ASCII upper alpha and append
  7888. // every other character unchanged, so a name of `Ð` stays `Ð`.
  7889. tok.name =
  7890. nameStart === -1
  7891. ? ""
  7892. : _asciiLowerCase(doctypePart(input, nameStart, nameEnd));
  7893. tok.publicId =
  7894. publicStart === -1 ? null : doctypePart(input, publicStart, publicEnd);
  7895. tok.systemId =
  7896. systemStart === -1 ? null : doctypePart(input, systemStart, systemEnd);
  7897. tok.forceQuirks = forceQuirks;
  7898. tok.start = start;
  7899. tok.end = end;
  7900. dispatch();
  7901. return end;
  7902. };
  7903. // A `<?…>` bogus comment is a processing instruction when its target is
  7904. // well-formed (§13.2.5). The tokenizer reports the source range; the DOM-level
  7905. // distinction is drawn here, so `tokenize` keeps reporting one comment token.
  7906. const PI_SCAN_COMMENT = 0;
  7907. const PI_SCAN_MATCH = 1;
  7908. // Cut short by EOF — the spec emits only an end-of-file token, so no node.
  7909. const PI_SCAN_DROP = 2;
  7910. // Sub-ranges of the instruction last scanned, read only right after a match.
  7911. let _piScanTargetEnd = 0;
  7912. let _piScanDataStart = 0;
  7913. let _piScanDataEnd = 0;
  7914. /**
  7915. * @param {string} input source
  7916. * @param {number} start offset of the `<`
  7917. * @param {number} end offset past the token
  7918. * @returns {number} one of the `PI_SCAN_*` outcomes
  7919. */
  7920. const _scanProcessingInstruction = (input, start, end) => {
  7921. const targetStart = start + 2;
  7922. // Nothing after `<?`, so EOF landed in the open state and drops the token.
  7923. if (targetStart >= end) return PI_SCAN_DROP;
  7924. const first = input.charCodeAt(targetStart);
  7925. if (!isAsciiAlpha(first) && first !== CC_LOW_LINE) return PI_SCAN_COMMENT;
  7926. let i = targetStart + 1;
  7927. while (i < end) {
  7928. const cc = input.charCodeAt(i);
  7929. if (isSpace(cc) || cc === CC_QUESTION_MARK || cc === CC_GREATER_THAN) break;
  7930. if (
  7931. !isAsciiAlphanumeric(cc) &&
  7932. cc !== CC_HYPHEN_MINUS &&
  7933. cc !== CC_LOW_LINE
  7934. ) {
  7935. return PI_SCAN_COMMENT;
  7936. }
  7937. i++;
  7938. }
  7939. // EOF while still reading the target drops the token, whatever it spells; a
  7940. // target that ends at a delimiter is judged below first.
  7941. if (i === end) return PI_SCAN_DROP;
  7942. // Reserved for XML, and common enough in legacy pages to keep as comments.
  7943. if (
  7944. rangeEqualsLowerCase(input, targetStart, i, "xml") ||
  7945. rangeEqualsLowerCase(input, targetStart, i, "xml-stylesheet")
  7946. ) {
  7947. return PI_SCAN_COMMENT;
  7948. }
  7949. _piScanTargetEnd = i;
  7950. // The rest ran into EOF instead of the closing `>`, so the token is dropped.
  7951. if (input.charCodeAt(end - 1) !== CC_GREATER_THAN) return PI_SCAN_DROP;
  7952. let contentEnd = end - 1;
  7953. while (i < contentEnd && isSpace(input.charCodeAt(i))) i++;
  7954. // A `?` right before the `>` closes the instruction instead of being data.
  7955. if (contentEnd > i && input.charCodeAt(contentEnd - 1) === CC_QUESTION_MARK) {
  7956. contentEnd--;
  7957. }
  7958. _piScanDataStart = i;
  7959. _piScanDataEnd = contentEnd;
  7960. return PI_SCAN_MATCH;
  7961. };
  7962. /** @type {(input: string, start: number, end: number, dataStart: number, dataEnd: number) => number} */
  7963. const handleCommentToken = (input, start, end, dataStart, dataEnd) => {
  7964. // Only the token immediately after the start tag may be the swallowed
  7965. // newline; anything else clears the flag (§13.2.6.4.7 "the next token").
  7966. swallowNextNewline = false;
  7967. // A CDATA section in foreign content arrives through this callback but is
  7968. // character data, not a comment.
  7969. if (
  7970. input.startsWith("<![CDATA[", start) &&
  7971. open.length > 0 &&
  7972. adjustedCurrent() &&
  7973. _namespaceOf(adjustedCurrent()) !== NS_HTML
  7974. ) {
  7975. const data = input.slice(dataStart, dataEnd).replace(/\r\n?/g, "\n");
  7976. if (data !== "") {
  7977. tok.type = TOKEN_CHAR;
  7978. tok.data = data;
  7979. tok.start = start;
  7980. tok.end = end;
  7981. dispatch();
  7982. }
  7983. return end;
  7984. }
  7985. if (input.charCodeAt(start + 1) === CC_QUESTION_MARK) {
  7986. const scan = _scanProcessingInstruction(input, start, end);
  7987. if (scan === PI_SCAN_DROP) return end;
  7988. if (scan === PI_SCAN_MATCH) {
  7989. tok.type = TOKEN_PROCESSING_INSTRUCTION;
  7990. tok.name = input.slice(start + 2, _piScanTargetEnd);
  7991. const piRaw = input.slice(_piScanDataStart, _piScanDataEnd);
  7992. tok.data = inputHasNul ? piRaw.replace(/\0/g, "\uFFFD") : piRaw;
  7993. tok.start = start;
  7994. tok.end = end;
  7995. dispatch();
  7996. return end;
  7997. }
  7998. }
  7999. tok.type = TOKEN_COMMENT;
  8000. const commentRaw = input.slice(dataStart, dataEnd);
  8001. tok.data = inputHasNul ? commentRaw.replace(/\0/g, "�") : commentRaw;
  8002. tok.start = start;
  8003. tok.end = end;
  8004. dispatch();
  8005. return end;
  8006. };
  8007. /** @type {(input: string, start: number, end: number) => number} */
  8008. const handleTextToken = (input, start, end) => {
  8009. // `skip.text` fast path: node dropped, so only whitespace-ness matters.
  8010. // With no `&`/`\0`/`\r` (and no pending newline-swallow) the decoded value
  8011. // equals the raw range — dispatch a canonical marker (`" "`/`"x"`) without
  8012. // the slice + entity decode; anything trickier falls through.
  8013. if (skipText && !swallowNextNewline) {
  8014. let hasNonWhitespace = false;
  8015. let i = start;
  8016. for (; i < end; i++) {
  8017. const charCode = input.charCodeAt(i);
  8018. // 2 = break (& / NUL / CR), 1 = whitespace, 0 = other.
  8019. const scanClass = charCode < 128 ? _TEXT_SCAN_CLASS[charCode] : 0;
  8020. if (scanClass === 2) break;
  8021. if (scanClass === 0) hasNonWhitespace = true;
  8022. }
  8023. if (i === end) {
  8024. tok.type = TOKEN_CHAR;
  8025. tok.data = hasNonWhitespace ? "x" : " ";
  8026. tok.start = start;
  8027. tok.end = end;
  8028. dispatch();
  8029. return end;
  8030. }
  8031. }
  8032. const raw = input.slice(start, end);
  8033. // CR normalization only when a CR is actually present (common case has none).
  8034. const normalized =
  8035. inputHasCr && raw.includes("\r") ? raw.replace(/\r\n?/g, "\n") : raw;
  8036. const top = adjustedCurrent();
  8037. const rawMode =
  8038. mode === MODE_TEXT ||
  8039. (top && _namespaceOf(top) === NS_HTML && _tagNameOf(top) === "plaintext");
  8040. const noDecode =
  8041. top &&
  8042. _namespaceOf(top) === NS_HTML &&
  8043. NO_DECODE_TEXT.has(_tagNameOf(top)) &&
  8044. (mode === MODE_TEXT || mode === MODE_IN_BODY);
  8045. let data = noDecode ? normalized : decode(normalized, false);
  8046. // In RAWTEXT/RCDATA/script/PLAINTEXT, NULL becomes U+FFFD (tokenizer
  8047. // rule); in data state NULLs pass through to be dropped in "in body".
  8048. if (rawMode && inputHasNul && data.includes("\0")) {
  8049. data = data.replace(/\0/g, "�");
  8050. }
  8051. // pre/listing/textarea swallow a leading newline (post entity decode).
  8052. if (swallowNextNewline) {
  8053. swallowNextNewline = false;
  8054. if (data[0] === "\n") data = data.slice(1);
  8055. }
  8056. if (data === "") return end;
  8057. tok.type = TOKEN_CHAR;
  8058. tok.data = data;
  8059. tok.start = start;
  8060. tok.end = end;
  8061. dispatch();
  8062. return end;
  8063. };
  8064. /** @type {(input: string, nameStart: number, nameEnd: number, valueStart: number, valueEnd: number, quoteType: number) => number} */
  8065. const handleAttributeToken = (
  8066. input,
  8067. nameStart,
  8068. nameEnd,
  8069. valueStart,
  8070. valueEnd,
  8071. quoteType
  8072. ) => {
  8073. const name = internLowerName(ATTR_NAME_INTERN, input, nameStart, nameEnd);
  8074. if (pendAttrStart === 0) pendAttrStart = _attrCount + 1;
  8075. // Drop duplicate attribute names (per spec). Plain loop avoids a
  8076. // per-attribute closure allocation on this hot path.
  8077. let dup = false;
  8078. for (let i = pendAttrStart; i <= _attrCount; i++) {
  8079. if (_attrNames[i] === name) {
  8080. dup = true;
  8081. break;
  8082. }
  8083. }
  8084. if (!dup) {
  8085. // The raw (undecoded) value is read from the source by offset on
  8086. // demand: consumers re-resolve requests from it and the offsets must
  8087. // stay aligned with the source. Only a valueless attribute stores an
  8088. // override ("").
  8089. _allocAttr(
  8090. name,
  8091. valueStart !== -1 ? null : "",
  8092. nameStart,
  8093. nameEnd,
  8094. valueStart,
  8095. valueEnd
  8096. );
  8097. }
  8098. if (valueStart === -1) return nameEnd;
  8099. return quoteType !== QUOTE_NONE ? valueEnd + 1 : valueEnd;
  8100. };
  8101. /** @type {(input: string, start: number, end: number, nameStart: number, nameEnd: number, selfClosing: boolean) => number} */
  8102. const handleStartTagToken = (
  8103. input,
  8104. start,
  8105. end,
  8106. nameStart,
  8107. nameEnd,
  8108. selfClosing
  8109. ) => {
  8110. // Only the token immediately after the start tag may be the swallowed
  8111. // newline; anything else clears the flag (§13.2.6.4.7 "the next token").
  8112. swallowNextNewline = false;
  8113. // A start tag the tokenizer only emitted because it hit EOF mid-tag
  8114. // is dropped, matching the spec's eof-in-tag handling.
  8115. if (eofInTag) {
  8116. // Any attribute slots already allocated for it are orphaned.
  8117. pendAttrStart = 0;
  8118. return end;
  8119. }
  8120. const name = internLowerName(TAG_NAME_INTERN, input, nameStart, nameEnd);
  8121. if (name === "selectedcontent") sawSelectedContent = true;
  8122. tok.type = TOKEN_START_TAG;
  8123. tok.name = name;
  8124. // The reused token carries the tag's attribute run; every consumer of
  8125. // `t.attrs` runs synchronously within this dispatch.
  8126. tok.attrs.start = pendAttrStart;
  8127. tok.attrs.count = pendAttrStart === 0 ? 0 : _attrCount + 1 - pendAttrStart;
  8128. pendAttrStart = 0;
  8129. tok.selfClosing = selfClosing;
  8130. tok.swallowNewline = false;
  8131. tok.pos.start = start;
  8132. tok.pos.end = end;
  8133. tok.pos.tagEnd = end;
  8134. tok.pos.nameEnd = nameEnd;
  8135. dispatch();
  8136. if (tok.swallowNewline) swallowNextNewline = true;
  8137. return end;
  8138. };
  8139. /** @type {(input: string, start: number, end: number, nameStart: number, nameEnd: number) => number} */
  8140. const handleEndTagToken = (input, start, end, nameStart, nameEnd) => {
  8141. // Only the token immediately after the start tag may be the swallowed
  8142. // newline; anything else clears the flag (§13.2.6.4.7 "the next token").
  8143. swallowNextNewline = false;
  8144. // Most end tags close the current element: compare the raw range against
  8145. // its already-interned tag name — one case-folding walk, no hash/probe.
  8146. let name;
  8147. if (open.length > 0) {
  8148. const topName = _nodeStrings[open[open.length - 1]];
  8149. if (rangeEqualsLowerCase(input, nameStart, nameEnd, topName)) {
  8150. name = topName;
  8151. }
  8152. }
  8153. if (name === undefined) {
  8154. name = internLowerName(TAG_NAME_INTERN, input, nameStart, nameEnd);
  8155. }
  8156. // End tags drop any parsed attributes (the slots are orphaned).
  8157. pendAttrStart = 0;
  8158. tok.type = TOKEN_END_TAG;
  8159. tok.name = name;
  8160. tok.pos.start = start;
  8161. tok.pos.end = end;
  8162. tok.pos.tagEnd = end;
  8163. tok.pos.nameEnd = nameEnd;
  8164. dispatch();
  8165. return end;
  8166. };
  8167. /** @type {HtmlTokenCallbacks} */
  8168. const PARSE_CALLBACKS = {
  8169. isForeign: currentNodeIsForeign,
  8170. vetoesContentMode,
  8171. fragmentContext: undefined,
  8172. parseError: handleParseError,
  8173. doctype: handleDoctypeToken,
  8174. comment: handleCommentToken,
  8175. text: handleTextToken,
  8176. attribute: handleAttributeToken,
  8177. openTag: handleStartTagToken,
  8178. closeTag: handleEndTagToken
  8179. };
  8180. /**
  8181. * @param {string} input HTML source
  8182. * @param {number=} pos start byte offset (string input only; default `0`)
  8183. * @param {HtmlParseOptions=} options parse options (fragment context, AST skips)
  8184. * @returns {HtmlDocument} ref to the document node — read through `A`; valid until the next parse
  8185. */
  8186. const parseHtml = (input, pos = 0, options = {}) => {
  8187. source = input;
  8188. inputHasCr = input.includes("\r");
  8189. inputHasNul = input.includes("\0");
  8190. // Only an unknown name's entry is a slice of the last document; one naming a
  8191. // known element is a shared constant, and stays warm across parses.
  8192. _dropSourceBackedNames(TAG_NAME_INTERN);
  8193. _dropSourceBackedNames(ATTR_NAME_INTERN);
  8194. const { fragmentContext, skip = EMPTY_SKIP } = options;
  8195. skipText = skip.text === true;
  8196. skipComments = skip.comments === true;
  8197. skipDoctype = skip.doctype === true;
  8198. _resetAstColumns();
  8199. _entered.length = 0;
  8200. _skipFrom = -1;
  8201. _streaming = false;
  8202. _docSkipped = false;
  8203. _openStackChanged = false;
  8204. _openSpliced = false;
  8205. _streamHalted = false;
  8206. // Pre-size the columns from the input length: a document big enough to
  8207. // exceed `_COLUMN_SHRINK_CAPACITY` re-enters with released (empty) columns,
  8208. // and growing through the doubling cascade re-allocates and copies every
  8209. // column several times per parse. ~12 bytes/node and ~50 bytes/attribute
  8210. // are the densest realistic HTML, so `len/12` (`len/48`) reaches the final
  8211. // capacity in one allocation; a sparser document over-allocates bounded
  8212. // scratch that the post-walk release frees, and the doubling growth remains
  8213. // as the safety net when the estimate is short. `skip.text` (what
  8214. // `HtmlParser` uses) drops every text node — roughly half of all nodes — so
  8215. // its node estimate uses `len/24`; attributes are unaffected by the skip and
  8216. // keep `len/48`.
  8217. let nodeEstimate = (input.length / (skipText ? 24 : 12)) | 0;
  8218. const attrEstimate = (input.length / 48) | 0;
  8219. if (_streamEligible) {
  8220. // Node ids are recycled once streaming starts, so a document past the
  8221. // threshold never needs the whole estimate — one allocation covering the
  8222. // activation point plus a batch of headroom is its high-water mark. A
  8223. // document below it is unaffected and still pre-sizes exactly. Attributes
  8224. // are not recycled (`_attrCount` only resets between parses), so their
  8225. // estimate stands either way.
  8226. const cap = _STREAM_MIN_NODES + _FLUSH_BATCH * 2;
  8227. if (nodeEstimate > cap) nodeEstimate = cap;
  8228. }
  8229. if (nodeEstimate > _nodeCapacity) _growNodeColumns(nodeEstimate, true);
  8230. if (attrEstimate > _attrCapacity) _growAttrColumns(attrEstimate, true);
  8231. _htmlSource = source;
  8232. doc = _allocNode(NodeType.Document, 0, 0);
  8233. _nodeStrings[doc] = "";
  8234. mode = MODE_INITIAL;
  8235. originalMode = 0;
  8236. open.length = 0;
  8237. afe.length = 0;
  8238. head = 0;
  8239. form = 0;
  8240. framesetOk = true;
  8241. pendAttrStart = 0;
  8242. fosterParenting = false;
  8243. _fosteredLog = null;
  8244. _printFromSource = false;
  8245. _fosteredRuns = null;
  8246. _fosteredNodes = null;
  8247. _fosterHolders = null;
  8248. // Node slots are reused between parses, so the marks come off with the sets.
  8249. if (_fosterVerbatim !== null) {
  8250. for (const n of _fosterVerbatim) _nodeFlags[n] &= ~FLAG_FOSTER_REGION;
  8251. }
  8252. _fosterVerbatim = null;
  8253. _fosterOpenTail = null;
  8254. _fosterSpanEnd = 0;
  8255. _fosterMoves = null;
  8256. templateModes.length = 0;
  8257. quirks = false;
  8258. fragment = 0;
  8259. tokenEnd = 0;
  8260. sharedPlace.parent = doc;
  8261. sharedPlace.beforeNode = 0;
  8262. pendingTableChars = null;
  8263. swallowNextNewline = false;
  8264. eofInTag = false;
  8265. sawSelectedContent = false;
  8266. // ---------- fragment setup ----------
  8267. if (fragmentContext) {
  8268. let ctxName = fragmentContext.toLowerCase();
  8269. let ctxNs = NS_HTML;
  8270. if (ctxName.startsWith("svg ")) {
  8271. ctxNs = NS_SVG;
  8272. ctxName = ctxName.slice(4);
  8273. } else if (ctxName.startsWith("math ")) {
  8274. ctxNs = NS_MATHML;
  8275. ctxName = ctxName.slice(5);
  8276. }
  8277. fragment = mkEl(ctxName, ctxNs, EMPTY_ATTRS, null);
  8278. const htmlEl = mkEl("html", NS_HTML, EMPTY_ATTRS, null);
  8279. _appendChild(doc, htmlEl);
  8280. open.push(htmlEl);
  8281. if (ctxNs !== NS_HTML) {
  8282. mode = MODE_IN_BODY;
  8283. } else if (["title", "textarea"].includes(ctxName)) {
  8284. originalMode = MODE_IN_BODY;
  8285. mode = MODE_TEXT;
  8286. } else if (
  8287. ["style", "xmp", "iframe", "noembed", "noframes", "script"].includes(
  8288. ctxName
  8289. )
  8290. ) {
  8291. originalMode = MODE_IN_BODY;
  8292. mode = MODE_TEXT;
  8293. } else if (ctxName === "noscript" || ctxName === "plaintext") {
  8294. mode = MODE_IN_BODY;
  8295. } else {
  8296. resetInsertionMode();
  8297. }
  8298. if (ctxName === "template") {
  8299. templateModes.push(MODE_IN_TEMPLATE);
  8300. mode = MODE_IN_TEMPLATE;
  8301. }
  8302. }
  8303. // Refresh the per-parse fragment context on the shared module-scope
  8304. // callbacks object; its function properties stay identity-stable so the
  8305. // tokenizer's callback call sites remain monomorphic.
  8306. PARSE_CALLBACKS.fragmentContext = fragment ? _tagNameOf(fragment) : undefined;
  8307. // A `skipChildren()` on the root means nothing under it is visited, so the
  8308. // walk never starts and only the document's own `exit` is left to fire.
  8309. if (_streamEligible && !_enterNode(doc)) {
  8310. _streamEligible = false;
  8311. _docSkipped = true;
  8312. }
  8313. tokenize(source, pos, PARSE_CALLBACKS);
  8314. tok.type = TOKEN_EOF;
  8315. dispatch();
  8316. // Not while printing: the mirror is content the engine makes, with no source
  8317. // text behind it, and the printer emits what it walks — so serializing it
  8318. // would write the selected option's subtree into the document, where the
  8319. // engine that reads the output mirrors it a second time.
  8320. if (sawSelectedContent && _writer === undefined) {
  8321. mirrorSelectedContent(doc, 0);
  8322. }
  8323. if (_streamEligible) _streamFinish(doc);
  8324. else if (_docSkipped) _exitNode(doc);
  8325. return doc;
  8326. };
  8327. /**
  8328. * Deep-clone a node into fresh ids, dropping attribute source offsets and the
  8329. * raw-text body span so cloned content does not re-emit dependencies.
  8330. * @param {HtmlNodeRef} node node
  8331. * @returns {HtmlNodeRef} clone
  8332. */
  8333. const cloneSubtree = (node) => {
  8334. const ty = _nodeTypes[node];
  8335. const clone = _allocNode(ty, _nodeStarts[node], _nodeEnds[node]);
  8336. _nodeStrings[clone] = _nodeStrings[node];
  8337. if (ty !== NodeType.Element) return clone;
  8338. _nodeFlags[clone] = _nodeFlags[node];
  8339. const attrs = cloneAttrs(node);
  8340. _nodeAttrStarts[clone] = attrs.start;
  8341. _nodeAttrCounts[clone] = attrs.count;
  8342. _nodeTagEnds[clone] = _nodeTagEnds[node];
  8343. _nodeNameEnds[clone] = _nodeNameEnds[node];
  8344. // Empty body span: the clone re-emits no raw-text dependency.
  8345. _nodeContentEnds[clone] = _nodeTagEnds[node];
  8346. for (let k = _nodeFirstChildren[node]; k !== 0; k = _nodeNextSiblings[k]) {
  8347. _appendChild(clone, cloneSubtree(k));
  8348. }
  8349. // Clone `<template>` content into the clone's own fragment.
  8350. const tc = _templateContentOf(node);
  8351. if (tc !== 0) {
  8352. const fragment = _allocNode(NodeType.DocumentFragment, 0, 0);
  8353. _nodeStrings[fragment] = "";
  8354. // Parent link so the iterative walk can ascend out of the content.
  8355. _nodeParents[fragment] = clone;
  8356. _nodeContentEnds[clone] = fragment;
  8357. _nodeFlags[clone] |= FLAG_HAS_TEMPLATE;
  8358. for (let k = _nodeFirstChildren[tc]; k !== 0; k = _nodeNextSiblings[k]) {
  8359. _appendChild(fragment, cloneSubtree(k));
  8360. }
  8361. }
  8362. return clone;
  8363. };
  8364. /**
  8365. * The selected option of a select: the last `<option selected>`, else the
  8366. * first option (scanning direct children and `<optgroup>` children).
  8367. * @param {HtmlElement} select select element
  8368. * @returns {HtmlElement} selected option (0 = none)
  8369. */
  8370. const selectedOption = (select) => {
  8371. /** @type {HtmlElement[]} */
  8372. const options = [];
  8373. /** @param {HtmlElement} el element */
  8374. const collect = (el) => {
  8375. for (let c = _nodeFirstChildren[el]; c !== 0; c = _nodeNextSiblings[c]) {
  8376. if (_nodeTypes[c] !== NodeType.Element || _namespaceOf(c) !== NS_HTML) {
  8377. continue;
  8378. }
  8379. if (_nodeStrings[c] === "option") options.push(c);
  8380. else if (_nodeStrings[c] === "optgroup") collect(c);
  8381. }
  8382. };
  8383. collect(select);
  8384. if (options.length === 0) return 0;
  8385. for (let i = options.length - 1; i >= 0; i--) {
  8386. const el = options[i];
  8387. if (_findAttr(_nodeAttrStarts[el], _nodeAttrCounts[el], "selected") !== 0) {
  8388. return el;
  8389. }
  8390. }
  8391. return options[0];
  8392. };
  8393. /**
  8394. * Fill each `<selectedcontent>` with a clone of its `<select>`'s selected
  8395. * option subtree (the customizable-select mirroring behavior). Not while
  8396. * printing: the clone has no source behind it, and the printer writes what it
  8397. * walks (see the call site).
  8398. * @param {HtmlNodeRef} node node
  8399. * @param {HtmlElement} select nearest ancestor select (0 = none)
  8400. */
  8401. const mirrorSelectedContent = (node, select) => {
  8402. // A `<template>`'s children live in its content fragment.
  8403. const tc = _templateContentOf(node);
  8404. const container = tc !== 0 ? tc : node;
  8405. for (
  8406. let child = _nodeFirstChildren[container];
  8407. child !== 0;
  8408. child = _nodeNextSiblings[child]
  8409. ) {
  8410. if (_nodeTypes[child] !== NodeType.Element) continue;
  8411. if (_namespaceOf(child) === NS_HTML && _nodeStrings[child] === "select") {
  8412. mirrorSelectedContent(child, child);
  8413. } else if (
  8414. select !== 0 &&
  8415. _namespaceOf(child) === NS_HTML &&
  8416. _nodeStrings[child] === "selectedcontent"
  8417. ) {
  8418. const option = selectedOption(select);
  8419. if (option !== 0) {
  8420. // Replace the children with clones of the option's subtree.
  8421. _nodeFirstChildren[child] = 0;
  8422. _nodeLastChildren[child] = 0;
  8423. for (
  8424. let k = _nodeFirstChildren[option];
  8425. k !== 0;
  8426. k = _nodeNextSiblings[k]
  8427. ) {
  8428. _appendChild(child, cloneSubtree(k));
  8429. }
  8430. // A `<select>` carried into the clone is a select of its own there, so
  8431. // its `<selectedcontent>` mirrors inside the copy as it would anywhere.
  8432. mirrorSelectedContent(child, 0);
  8433. }
  8434. } else if (
  8435. select !== 0 &&
  8436. _namespaceOf(child) === NS_HTML &&
  8437. _nodeStrings[child] === "option"
  8438. ) {
  8439. // An option is what a `<selectedcontent>` mirrors, so nothing inside one
  8440. // mirrors: not a `<selectedcontent>` written there, and not a `<select>`
  8441. // nested under it either.
  8442. continue;
  8443. } else {
  8444. mirrorSelectedContent(child, select);
  8445. }
  8446. }
  8447. };
  8448. /** @typedef {HtmlNode | HtmlDocument | HtmlDocumentFragment} HtmlVisitableNode */
  8449. // HTML-typed views over the generic visitor machinery (`util/SourceProcessor`).
  8450. /**
  8451. * @typedef {import("../util/SourceProcessor").VisitorFn<HtmlPath>} VisitorFn
  8452. * @typedef {import("../util/SourceProcessor").VisitorBucket<HtmlPath>} VisitorBucket
  8453. * @typedef {import("../util/SourceProcessor").VisitorMap<HtmlPath>} VisitorMap
  8454. * @typedef {import("../util/SourceProcessor").CompiledVisitorMap<HtmlPath>} CompiledVisitorMap
  8455. */
  8456. /**
  8457. * What the minifying printer may rewrite. Every entry is on unless it is
  8458. * `false`, so a document one transform breaks can still be minified by the rest.
  8459. * @typedef {object} HtmlTransformOptions
  8460. * @property {boolean=} collapseBooleanAttributes write a boolean attribute spelled with its own name (`disabled="disabled"`) as the bare name
  8461. * @property {(boolean | "all" | "some" | string | RegExp | ((comment: string) => boolean))=} comments which comments survive: `"some"` (the default) the ones that carry something, `true` / `"all"` every one, `false` none, or the ones a pattern matches / a predicate accepts, over the comment's own text. A comment a parser or a server reads is kept whatever this says
  8462. * @property {boolean=} minifyJson strip the whitespace between a JSON `<script>`'s tokens
  8463. * @property {boolean=} minifyStyles run the CSS minifier over an inline `<style>` and every `style=""`
  8464. * @property {boolean=} normalizeAttributeQuotes drop or re-pick an attribute value's quotes
  8465. * @property {boolean=} normalizeEnumeratedAttributes fold an enumerated value to the keyword it names
  8466. * @property {boolean=} normalizeListAttributes normalize a space-, comma- or descriptor-separated list value (`class`, `rel`, `srcset`, `sizes`, the viewport `content`)
  8467. * @property {boolean=} normalizeNumericAttributes write an integer attribute the one way its rules read it
  8468. * @property {boolean=} removeOptionalTags leave out an optional tag other than the `<html>` / `<head>` / `<body>` shell, which is `removeImpliedTags`
  8469. */
  8470. /**
  8471. * @typedef {object} HtmlProcessOptions
  8472. * @property {string=} fragmentContext context element tag name for fragment parsing (see `parseHtml`); the HTML analog of the CSS parser's `as` parse-mode option
  8473. * @property {HtmlAstSkip=} skip node kinds to omit from the AST for speed/memory (see `HtmlAstSkip`)
  8474. * @property {boolean=} minimize print the safely-minified serialization (nodes rebuilt from source, inert comments dropped, opening-tag whitespace collapsed) as `process` walks, and return it (default false = walk only, return `""`)
  8475. * @property {CssEnvironment=} environment CSS's, not HTML's: handed to the CSS minifier that runs over an inline `<style>` and every `style=""`, so the inline copy of a declaration agrees with the `.css` asset
  8476. * @property {boolean=} convertLengthUnits CSS's too, handed over with `environment` (see `HtmlPrintOptions`)
  8477. * @property {boolean=} rewriteCustomProperties CSS's too, handed over with `environment` (see `HtmlPrintOptions`)
  8478. * @property {CssTransformOptions=} cssTransforms CSS's per-transform switches too, handed over with `environment` (see `HtmlPrintOptions`)
  8479. * @property {HtmlTransformOptions=} transforms which of the meaning-preserving rewrites the minifying print makes; each is on unless it is `false`
  8480. * @property {(boolean | "conservative" | "smart" | "all")=} collapseWhitespace collapse each run of whitespace in text to a single space, except where an ancestor renders it verbatim; `"smart"` also drops what sits against a block edge and `"all"` drops every edge (default false)
  8481. * @property {boolean=} removeEmptyAttributes drop an attribute whose empty value leaves it in the state its absence gives (default false)
  8482. * @property {boolean=} removeEmptyElements drop an element with no children and no attributes, unless its bare form is meaningful (default false)
  8483. * @property {boolean=} mergeStyles print a run of adjacent `<style>` elements as one sheet, which removes elements (default false)
  8484. * @property {boolean=} sortAttributes print an element's attributes commonest name first, ties by name, which nothing in HTML reads (default false)
  8485. * @property {boolean=} sortTokenLists print every token list the DOM reads as a set (`class`, `rel`, `part`, …) in token order (default false)
  8486. * @property {(boolean | "smart" | "all")=} removeRedundantAttributes drop an attribute whose value is the element's own default; `true` is `"smart"`, and `"all"` also drops the spec defaults a selector can match (default false)
  8487. * @property {(boolean | "smart" | "all")=} removeImpliedTags how much of the `<html>` / `<head>` / `<body>` shell §13.1.2.4 lets the parser imply may be left out: `"smart"` leaves out only the `<html>` start tag, `true` (or `"all"`) all six, `false` none (default `"smart"`)
  8488. * @property {boolean=} deferSrcdoc whether an `<iframe srcdoc>` is among what `deferEmbeddedSource` collects (default true); false for a caller that minifies them itself, which keeps the attribute on the normal path and its shorter delimiter
  8489. * @property {DeferredEmbeddedSource[]=} deferEmbeddedSource collects what `renderEmbeddedSource` would be offered instead of offering it, for a caller whose renderer is asynchronous: the print leaves a marker for each and `finish` puts the answers in their place, so one parse serves both. A `style=""` stays with the built-in CSS minifier, whose text this print reads back to decide how the attribute is written. Takes precedence over `renderEmbeddedSource`
  8490. * @property {EmbeddedSourceRenderer=} renderEmbeddedSource renders each nested body this document embeds — an inline `<style>`, every `style=""` (handed over as a whole stylesheet, SVG's and MathML's included), a `<script>` holding JSON or JavaScript, an `<svg>` subtree, and the document an `<iframe srcdoc>` holds (decoded, and written back escaped). Replaces the built-in CSS and JSON minifiers wherever it answers, and returning anything but text falls back to them; it is the only way inline JavaScript, SVG and a nested document are reached at all
  8491. */
  8492. /**
  8493. * What the HTML printer may be told, on top of the `mode` every language has.
  8494. * The first two are not HTML's own: an inline `<style>` and every `style=""` run
  8495. * through the CSS minifier, so `optimization.minimize.css` rides along to reach
  8496. * it (see `lib/config/defaults.js`) and the two must not disagree about what the
  8497. * target can read. The rest is `optimization.minimize.html`.
  8498. * @typedef {Pick<CssProcessOptions, "environment" | "convertLengthUnits" | "rewriteCustomProperties"> & { cssTransforms?: CssTransformOptions, transforms?: HtmlTransformOptions, collapseWhitespace?: boolean | "conservative" | "smart" | "all", mergeStyles?: boolean, removeEmptyAttributes?: boolean, removeEmptyElements?: boolean, removeRedundantAttributes?: boolean | "smart" | "all", sortAttributes?: boolean, sortTokenLists?: boolean, removeImpliedTags?: boolean | "smart" | "all", renderEmbeddedSource?: EmbeddedSourceRenderer, deferEmbeddedSource?: DeferredEmbeddedSource[], deferSrcdoc?: boolean }} HtmlPrintOptions
  8499. */
  8500. /** @typedef {import("../util/SourceProcessor").PrintContext<HtmlPath, HtmlNodeRef, HtmlPrintOptions>} PrintContext */
  8501. // Whether the text ends inside an unterminated character reference, so the next
  8502. // sibling's first characters could complete it (`a &am` + `p;`).
  8503. /**
  8504. * @param {string} s source text
  8505. * @returns {boolean} true when a character reference is still open at the end
  8506. */
  8507. const _hasOpenReference = (s) => {
  8508. const last = s.lastIndexOf("&");
  8509. if (last === -1) return false;
  8510. // Read in place: slicing the tail off to match it against a pattern allocates
  8511. // a string per text node, and every one of them is thrown away here.
  8512. for (let i = last + 1; i < s.length; i++) {
  8513. const c = s.charCodeAt(i);
  8514. if (!isAsciiAlphanumeric(c) && c !== CC_NUMBER_SIGN) return false;
  8515. }
  8516. return true;
  8517. };
  8518. /**
  8519. * Whether the `&` at `i` would be consumed as a character reference when the
  8520. * text is read back (§13.2.5.72): only before `#` or a name the table holds.
  8521. * Every other `&` the tokenizer flushes as literal text, so `R&D` needs no
  8522. * escape — and neither does the `&` in a query string.
  8523. * @param {string} s decoded text
  8524. * @param {number} i the `&`'s index
  8525. * @returns {boolean} true when it has to be escaped
  8526. */
  8527. const _startsCharacterReference = (s, i) => {
  8528. // Nothing terminates the run before the node ends, so the next sibling's
  8529. // first characters could complete it once a comment between them is dropped
  8530. // (`a &am` + `p;`) — the same reason the source fast path keeps out.
  8531. let end = i + 1;
  8532. while (end < s.length) {
  8533. const c = s.charCodeAt(end);
  8534. if (!isAsciiAlphanumeric(c) && c !== 0x23) break;
  8535. end++;
  8536. }
  8537. if (end === s.length) return true;
  8538. const next = s.charCodeAt(i + 1);
  8539. // `#` opens a numeric reference.
  8540. if (next === 0x23) return true;
  8541. if (!isAsciiAlphanumeric(next)) return false;
  8542. // The named-reference table holds the names with and without their `;`, so
  8543. // any prefix hit is a reference the tokenizer would take.
  8544. const longest = Math.min(s.length, i + 1 + MAX_ENTITY_NAME_LEN);
  8545. for (let j = i + 2; j <= longest; j++) {
  8546. if (HTML_ENTITIES[s.slice(i + 1, j)] !== undefined) return true;
  8547. }
  8548. return false;
  8549. };
  8550. /**
  8551. * Escape a text node's decoded data for re-serialization: `&` and `<`, which
  8552. * start a character reference and a tag, plus `>` outside minification, where
  8553. * the §13.3 serialization is followed to the letter. A CR is written as a
  8554. * reference either way — §13.2.3.5 rewrites a literal one to LF before the
  8555. * tokenizer reads it. Unlike the exported `escapeText` (which also numerically
  8556. * encodes newlines and U+00A0 for single-line `data:` URIs), this keeps
  8557. * newlines and U+00A0 literal — byte-lean and the same DOM.
  8558. * @param {string} s decoded text
  8559. * @param {boolean} minify whether the shorter (still equivalent) form is wanted
  8560. * @returns {string} text-content-safe string
  8561. */
  8562. const _escapeTextContent = (s, minify) => {
  8563. // A bare `>` is only ever a character in text, so §13.3's escape of it buys
  8564. // nothing but bytes. `<` always starts a tag, and an `&` only sometimes
  8565. // starts a character reference — §13.2.5.72 flushes the rest as literal text.
  8566. if (minify) {
  8567. if (!s.includes("&") && !s.includes("<") && !s.includes("\r")) return s;
  8568. let out = "";
  8569. let last = 0;
  8570. for (let i = 0; i < s.length; i++) {
  8571. const c = s.charCodeAt(i);
  8572. if (c === 0x3c) {
  8573. out += `${s.slice(last, i)}&lt;`;
  8574. last = i + 1;
  8575. } else if (c === 0x26 && _startsCharacterReference(s, i)) {
  8576. out += `${s.slice(last, i)}&amp;`;
  8577. last = i + 1;
  8578. } else if (c === 0x0d) {
  8579. // §13.2.3.5 rewrites a literal CR to LF before the tokenizer sees it,
  8580. // so one a character reference put in the text has to stay one.
  8581. out += `${s.slice(last, i)}&#13;`;
  8582. last = i + 1;
  8583. }
  8584. }
  8585. return last === 0 ? s : out + s.slice(last);
  8586. }
  8587. if (
  8588. !s.includes("&") &&
  8589. !s.includes("<") &&
  8590. !s.includes(">") &&
  8591. !s.includes("\r")
  8592. ) {
  8593. return s;
  8594. }
  8595. return s
  8596. .replace(/&/g, "&amp;")
  8597. .replace(/</g, "&lt;")
  8598. .replace(/>/g, "&gt;")
  8599. .replace(/\r/g, "&#13;");
  8600. };
  8601. /**
  8602. * Rebuild an opening tag from the node's name and attributes, for elements with
  8603. * no matching source tag (adoption-agency clones, reconstructed formatting
  8604. * elements, renamed tokens like `<image>`). Attribute values are raw
  8605. * (undecoded), so they re-parse to the original decoded values.
  8606. * @param {HtmlPath} path the accessor positioned on the element
  8607. * @returns {string} serialized opening tag
  8608. */
  8609. const _synthesizeOpenTag = (path) => {
  8610. let out = `<${path.tagName()}`;
  8611. for (const attribute of path.attributes()) {
  8612. const name =
  8613. attribute.serializedName !== undefined
  8614. ? attribute.serializedName
  8615. : attribute.name;
  8616. out +=
  8617. attribute.value === ""
  8618. ? ` ${name}`
  8619. : ` ${name}="${attribute.value.replace(/"/g, "&quot;")}"`;
  8620. }
  8621. return `${out}>`;
  8622. };
  8623. /**
  8624. * Whether an element's default display puts whitespace at its edge outside every
  8625. * line box, so none of it renders.
  8626. * @param {HtmlPath} path the accessor
  8627. * @param {HtmlNodeRef} node node (0 = none)
  8628. * @returns {boolean} true when it is a block-level HTML element
  8629. */
  8630. const _isBlockLevel = (path, node) =>
  8631. node !== 0 &&
  8632. path.type(node) === NodeType.Element &&
  8633. path.namespace(node) === NS_HTML &&
  8634. BLOCK_LEVEL_ELEMENTS.has(path.tagName(node));
  8635. /**
  8636. * Whether a text node's leading whitespace sits outside every line box, which is
  8637. * so when it opens a block container. A block *sibling* in front of it would do
  8638. * the same, but reading backwards costs a scan of the whole child list — so this
  8639. * only keeps a space that renders, never drops one that does not.
  8640. * @param {HtmlPath} path the accessor
  8641. * @param {HtmlNodeRef} node the text node
  8642. * @param {HtmlNodeRef} parent its parent (0 = none)
  8643. * @returns {boolean} true when the leading whitespace renders as nothing
  8644. */
  8645. const _startsOutsideLineBox = (path, node, parent) =>
  8646. parent !== 0 &&
  8647. _nodeFirstChildren[parent] === node &&
  8648. _isBlockLevel(path, parent);
  8649. /**
  8650. * Whether a text node's trailing whitespace sits outside every line box: it
  8651. * closes a block container, or a block element follows it.
  8652. * @param {HtmlPath} path the accessor
  8653. * @param {HtmlNodeRef} node the text node
  8654. * @param {HtmlNodeRef} parent its parent (0 = none)
  8655. * @returns {boolean} true when the trailing whitespace renders as nothing
  8656. */
  8657. const _endsOutsideLineBox = (path, node, parent) => {
  8658. const next = path.nextSibling(node);
  8659. if (next === 0) return parent !== 0 && _isBlockLevel(path, parent);
  8660. return _isBlockLevel(path, next);
  8661. };
  8662. /**
  8663. * Whether a node prints nothing at all, so what follows it in the output is its
  8664. * next sibling. The two that can: a comment the inert-comment rule drops, and a
  8665. * whitespace-only text node the active `collapseWhitespace` tier deletes — the
  8666. * same two questions the printer asks when it reaches them, asked here so the
  8667. * §13.1.2.4 tests below read the output rather than the tree.
  8668. * @param {HtmlPath} path the accessor
  8669. * @param {HtmlNodeRef} node the node
  8670. * @returns {boolean} true when it contributes no output
  8671. */
  8672. const _printsNothing = (path, node) => {
  8673. const type = path.type(node);
  8674. if (type === NodeType.Comment) {
  8675. return _minifying && !_keepComment(path.data(node), path.source(node));
  8676. }
  8677. if (type !== NodeType.Text) return false;
  8678. const data = path.data(node);
  8679. if (!isAllWs(data)) return false;
  8680. const parent = path.parentOf(node);
  8681. if (parent === 0 || path.namespace(parent) !== NS_HTML) return false;
  8682. const parentName = path.tagName(parent);
  8683. // A literal-text body is data — its whitespace is printed as written.
  8684. if (LITERAL_TEXT_PARENTS.has(parentName)) return false;
  8685. // Outside any block formatting context, so nothing ever renders it — which is
  8686. // a reason to drop it, not a claim that a re-serializing pass already did.
  8687. if (parentName === "head" || parentName === "html") return _minifying;
  8688. if (
  8689. _collapseWhitespace === "none" ||
  8690. _collapseWhitespace === "conservative"
  8691. ) {
  8692. return false;
  8693. }
  8694. for (
  8695. let ancestor = parent;
  8696. ancestor !== 0;
  8697. ancestor = path.parentOf(ancestor)
  8698. ) {
  8699. if (
  8700. path.namespace(ancestor) === NS_HTML &&
  8701. LEADING_NEWLINE_ELEMENTS.has(path.tagName(ancestor))
  8702. ) {
  8703. return false;
  8704. }
  8705. }
  8706. // Whitespace-only, so it collapses to one space; either edge going empties it.
  8707. return (
  8708. _collapseWhitespace === "all" ||
  8709. _startsOutsideLineBox(path, node, parent) ||
  8710. _endsOutsideLineBox(path, node, parent)
  8711. );
  8712. };
  8713. /**
  8714. * The next sibling that prints something, skipping the ones that print nothing.
  8715. * @param {HtmlPath} path the accessor
  8716. * @param {HtmlNodeRef} node the node to start after
  8717. * @returns {HtmlNodeRef} that sibling, or 0
  8718. */
  8719. const _nextPrintedSibling = (path, node) => {
  8720. let next = path.nextSibling(node);
  8721. while (next !== 0 && _printsNothing(path, next)) {
  8722. next = path.nextSibling(next);
  8723. }
  8724. return next;
  8725. };
  8726. // The elements whose start tag closes the one before it only inside a `ruby`.
  8727. const RUBY_SEGMENTS = new Set(["rb", "rp", "rt", "rtc"]);
  8728. /**
  8729. * Whether a `ruby` is in scope above `node`. A `<rb>` / `<rt>` / `<rp>` start tag
  8730. * only closes the one before it when there is one (§13.2.6.4.7), so without it
  8731. * an omitted end tag nests the next element inside this one instead.
  8732. * @param {HtmlPath} path the accessor
  8733. * @param {HtmlNodeRef} node the element
  8734. * @returns {boolean} true when a `ruby` encloses it
  8735. */
  8736. const _hasRubyInScope = (path, node) => {
  8737. for (
  8738. let parent = path.parentOf(node);
  8739. parent !== 0 && path.type(parent) === NodeType.Element;
  8740. parent = path.parentOf(parent)
  8741. ) {
  8742. if (path.namespace(parent) !== NS_HTML) return false;
  8743. const name = path.tagName(parent);
  8744. if (name === "ruby") return true;
  8745. if (HTML_SCOPE.has(name)) return false;
  8746. }
  8747. return false;
  8748. };
  8749. /**
  8750. * Whether an element's end tag may be omitted (§13.1.2.4). Reads what will be
  8751. * printed rather than the tree: a comment minifying drops, and whitespace the
  8752. * collapse tier deletes, are not content the tag has to stay for.
  8753. * @param {HtmlPath} path the accessor positioned on the element
  8754. * @param {string} name the element's lowercased tag name
  8755. * @param {HtmlNodeRef} node the element
  8756. * @returns {boolean} true when the end tag can be left out
  8757. */
  8758. const _canOmitEndTag = (path, name, node) => {
  8759. if (path.namespace(node) !== NS_HTML) return false;
  8760. if (name === "html" || name === "head" || name === "body") {
  8761. return _removeImpliedTags !== "none" && _canOmitShellEnd(path, name, node);
  8762. }
  8763. if (!_transforms.removeOptionalTags) return false;
  8764. if (OPTIONAL_END_TAG_UNLESS_TRAILING_NODE.has(name)) {
  8765. // Whitespace or a comment behind it would move inside once the tag goes;
  8766. // an element closes it through the insertion mode instead.
  8767. const trailing = _nextPrintedSibling(path, node);
  8768. if (trailing === 0) return true;
  8769. if (path.type(trailing) !== NodeType.Element) return false;
  8770. // A `<template>` does not close what it stands behind: §13.2.6.4.12 hands it
  8771. // to the head rules, which insert it where it is.
  8772. return !(
  8773. path.namespace(trailing) === NS_HTML &&
  8774. path.tagName(trailing) === "template"
  8775. );
  8776. }
  8777. const followers = OPTIONAL_END_TAG_FOLLOWERS.get(name);
  8778. if (followers === undefined) return false;
  8779. const next = _nextPrintedSibling(path, node);
  8780. if (next === 0) {
  8781. if (!OPTIONAL_END_TAG_AT_END.has(name)) return false;
  8782. const parent = path.parentOf(node);
  8783. const parentIsHtml = parent !== 0 && path.namespace(parent) === NS_HTML;
  8784. // Left open inside a formatting element, this is the furthest block the
  8785. // adoption agency needs — the parent's end tag would then restructure both.
  8786. if (
  8787. parentIsHtml &&
  8788. SPECIAL.has(name) &&
  8789. FORMATTING.has(path.tagName(parent))
  8790. ) {
  8791. return false;
  8792. }
  8793. if (!parentIsHtml) return false;
  8794. // A `<template>`'s content is a fragment, not an element, and `</template>`
  8795. // closes what is open inside it.
  8796. if (path.type(parent) !== NodeType.Element) return true;
  8797. // "Any other end tag" walks past a node that is not special, so a parent's
  8798. // end tag closes one of those whatever the parent is.
  8799. if (!SPECIAL.has(name)) return true;
  8800. // A special one blocks that walk instead, so only a parent whose end tag
  8801. // generates implied end tags closes it. Under any other — `<canvas>`, a
  8802. // custom element — it stays open and swallows what follows the parent.
  8803. return P_ENDS_ON_PARENT_END_TAG.has(path.tagName(parent));
  8804. }
  8805. if (
  8806. path.type(next) !== NodeType.Element ||
  8807. path.namespace(next) !== NS_HTML ||
  8808. !followers.has(path.tagName(next))
  8809. ) {
  8810. return false;
  8811. }
  8812. // A `<table>` start tag closes an open `p` only outside quirks mode — the
  8813. // one follower whose reading the document mode decides.
  8814. if (quirks && name === "p" && path.tagName(next) === "table") return false;
  8815. // Outside a `ruby` the follower's start tag closes nothing, so this one would
  8816. // go on to hold it.
  8817. if (RUBY_SEGMENTS.has(name) && !_hasRubyInScope(path, node)) return false;
  8818. // A parser-implied `<tbody>` / `<tr>` is printed transparently, so there is
  8819. // no start tag to close this element — its rows would continue this one.
  8820. return (
  8821. path.openTag(next) !== "" ||
  8822. path.attributeCount(next) !== 0 ||
  8823. !TRANSPARENT_IMPLIED_ELEMENTS.has(path.tagName(next))
  8824. );
  8825. };
  8826. /**
  8827. * The last sibling before `node` that prints something, or 0 — the accessor only
  8828. * walks forward, so this walks the chain. Skipping what prints nothing is what
  8829. * keeps it the mirror of `_nextPrintedSibling`: the two decide whether one of
  8830. * `</thead>` and a `<tbody>` start tag may go, and reading a dropped whitespace
  8831. * node from one side only would let both go and merge the rows into the head.
  8832. * @param {HtmlPath} path the accessor
  8833. * @param {HtmlNodeRef} node the element
  8834. * @returns {HtmlNodeRef} the preceding printed sibling, or 0
  8835. */
  8836. const _previousPrintedSibling = (path, node) => {
  8837. let previous = 0;
  8838. for (
  8839. let c = path.firstChild(path.parentOf(node));
  8840. c !== 0 && c !== node;
  8841. c = path.nextSibling(c)
  8842. ) {
  8843. if (!_printsNothing(path, c)) previous = c;
  8844. }
  8845. return previous;
  8846. };
  8847. /**
  8848. * Whether an element is shaped like one the parser would re-imply: no attributes
  8849. * to carry, and `child` opening it. Anything before that child (whitespace, a
  8850. * comment) would land outside the implied element, so it blocks the omission.
  8851. * @param {HtmlPath} path the accessor
  8852. * @param {HtmlNodeRef} node the element
  8853. * @param {string} child the tag name the implied element must start with
  8854. * @returns {boolean} true when the shape allows dropping the start tag
  8855. */
  8856. const _impliedStartTagShape = (path, node, child) => {
  8857. if (path.attributeCount(node) !== 0 || path.namespace(node) !== NS_HTML) {
  8858. return false;
  8859. }
  8860. const first = path.firstChild(node);
  8861. return (
  8862. first !== 0 &&
  8863. path.type(first) === NodeType.Element &&
  8864. path.namespace(first) === NS_HTML &&
  8865. path.tagName(first) === child
  8866. );
  8867. };
  8868. /**
  8869. * Whether a `<tbody>` start tag may be omitted (§13.1.2.4): the parser re-implies
  8870. * it when a `<tr>` opens a table section, but only if no earlier section ran into
  8871. * it — a preceding `tbody` / `thead` / `tfoot` that also dropped its end tag would
  8872. * swallow these rows instead.
  8873. * @param {HtmlPath} path the accessor positioned on the element
  8874. * @param {HtmlNodeRef} node the element
  8875. * @returns {boolean} true when the start tag can be left out
  8876. */
  8877. const _canOmitTbodyStart = (path, node) => {
  8878. if (!_impliedStartTagShape(path, node, "tr")) return false;
  8879. const previous = _previousPrintedSibling(path, node);
  8880. if (previous === 0 || path.type(previous) !== NodeType.Element) return true;
  8881. const previousName = path.tagName(previous);
  8882. if (
  8883. previousName !== "tbody" &&
  8884. previousName !== "thead" &&
  8885. previousName !== "tfoot"
  8886. ) {
  8887. return true;
  8888. }
  8889. return !_canOmitEndTag(path, previousName, previous);
  8890. };
  8891. /**
  8892. * Whether a `<colgroup>` start tag may be omitted (§13.1.2.4): a `<col>` in a
  8893. * table implies one, but only when the `<col>` is the first thing inside and no
  8894. * preceding `<colgroup>` dropped its end tag — that one would take these columns.
  8895. * @param {HtmlPath} path the accessor positioned on the element
  8896. * @param {HtmlNodeRef} node the element
  8897. * @returns {boolean} true when the start tag can be left out
  8898. */
  8899. const _canOmitColgroupStart = (path, node) => {
  8900. if (!_impliedStartTagShape(path, node, "col")) return false;
  8901. const previous = _previousPrintedSibling(path, node);
  8902. if (
  8903. previous === 0 ||
  8904. path.type(previous) !== NodeType.Element ||
  8905. path.tagName(previous) !== "colgroup"
  8906. ) {
  8907. return true;
  8908. }
  8909. return !_canOmitEndTag(path, "colgroup", previous);
  8910. };
  8911. // The CSS abilities of the target, forwarded into the nested CSS minifier so an
  8912. // inline `<style>` or `style=""` is gated exactly like a `.css` asset. Set per
  8913. // print, since the printer is the only place the options are in scope.
  8914. /** @type {CssEnvironment | undefined} */
  8915. let _cssEnvironment;
  8916. // Whether the nested CSS minifier may rewrite a length's unit — forwarded like
  8917. // `_cssEnvironment` so inline CSS agrees with the `.css` assets.
  8918. let _cssConvertLengthUnits = false;
  8919. // And whether it may shorten a custom property's value, forwarded the same way.
  8920. let _cssRewriteCustomProperties = false;
  8921. // And which of its rewrites it makes at all, forwarded the same way — inline CSS
  8922. // is minified by the same switches a `.css` asset is.
  8923. /** @type {CssTransformOptions | undefined} */
  8924. let _cssTransforms;
  8925. // Every rewrite on, which is what a print with no `transforms` option makes and
  8926. // what the walk-only passes hold. Frozen, since it is shared rather than copied.
  8927. /** @type {Required<HtmlTransformOptions>} */
  8928. const _ALL_TRANSFORMS = Object.freeze({
  8929. collapseBooleanAttributes: true,
  8930. comments: "some",
  8931. minifyJson: true,
  8932. minifyStyles: true,
  8933. normalizeAttributeQuotes: true,
  8934. normalizeEnumeratedAttributes: true,
  8935. normalizeListAttributes: true,
  8936. normalizeNumericAttributes: true,
  8937. removeOptionalTags: true
  8938. });
  8939. /**
  8940. * The rewrites this print makes, resolved once: each name is what the options
  8941. * set it to, and what `_ALL_TRANSFORMS` gives it where they set nothing — so no
  8942. * read of the result asks whether the option was given. `comments` keeps
  8943. * whatever it was given, which is still falsy only when it is `false`.
  8944. *
  8945. * Always a copy, and always made the one way: spreading the table and then
  8946. * writing over a name it already has leaves the object's shape alone, so every
  8947. * read of the result — and there is one per token — sees a single hidden class.
  8948. * Handing the frozen table itself back where nothing is set would save this
  8949. * object and cost each of those reads a second shape, `Object.freeze` giving a
  8950. * frozen object a map of its own.
  8951. * @param {HtmlTransformOptions | undefined} options the `transforms` option
  8952. * @returns {Required<HtmlTransformOptions>} every rewrite resolved
  8953. */
  8954. const _transformsFrom = (options) => {
  8955. /** @type {EXPECTED_ANY} */
  8956. const out = { ..._ALL_TRANSFORMS };
  8957. if (options !== undefined) {
  8958. for (const name of Object.keys(_ALL_TRANSFORMS)) {
  8959. const value = /** @type {EXPECTED_ANY} */ (options)[name];
  8960. if (value !== undefined) out[name] = value;
  8961. }
  8962. }
  8963. return out;
  8964. };
  8965. // The same table, as a copy, for the reason `_transformsFrom` always makes one.
  8966. /** @type {Required<HtmlTransformOptions>} */
  8967. const _DEFAULT_TRANSFORMS = _transformsFrom(undefined);
  8968. /**
  8969. * The `comments` option resolved to what it means per comment: `true` every
  8970. * one, `false` none, `"some"` the ones that carry something, or a predicate
  8971. * over the comment's own text — which is what a pattern compiles to here, so no
  8972. * print re-reads one per comment. A pattern is matched from the start each
  8973. * time, since a `g` flag would otherwise carry an index between comments.
  8974. * @param {Required<HtmlTransformOptions>["comments"]} comments the option
  8975. * @returns {boolean | "some" | ((comment: string) => boolean)} what it keeps
  8976. */
  8977. const _keptComments = (comments) => {
  8978. if (comments === "all") return true;
  8979. if (typeof comments === "boolean" || comments === "some") return comments;
  8980. if (typeof comments === "function") return comments;
  8981. const pattern =
  8982. typeof comments === "string" ? new RegExp(comments) : comments;
  8983. return (comment) => {
  8984. pattern.lastIndex = 0;
  8985. return pattern.test(comment);
  8986. };
  8987. };
  8988. // Which rewrites this print makes. One object rather than a flag apiece: it is
  8989. // read straight off the options, and a fixed shape keeps each read monomorphic.
  8990. /** @type {Required<HtmlTransformOptions>} */
  8991. let _transforms = _DEFAULT_TRANSFORMS;
  8992. /**
  8993. * The per-transform switches out of a wider options object — what
  8994. * `optimization.minimize.html` names beside the transforms that are off until
  8995. * asked for. Exported so the minify function the minimizer plugin ships to its
  8996. * workers can pick them without repeating the names.
  8997. * @param {EXPECTED_OBJECT} options an options object that may carry them
  8998. * @returns {HtmlTransformOptions | undefined} the switches, or undefined when none is set
  8999. */
  9000. const pickTransforms = (options) => {
  9001. /** @type {EXPECTED_ANY} */
  9002. let out;
  9003. for (const name of Object.keys(_ALL_TRANSFORMS)) {
  9004. const value = /** @type {EXPECTED_ANY} */ (options)[name];
  9005. if (value === undefined) continue;
  9006. if (out === undefined) out = {};
  9007. out[name] = value;
  9008. }
  9009. return out;
  9010. };
  9011. // Minified `style` attributes by raw text — a pure function of the value and
  9012. // the options above, which a document repeats far more often than it adds one.
  9013. /** @type {Map<string, string>} */
  9014. const _styleAttributeCache = new Map();
  9015. // How far text whitespace may be collapsed (see `HtmlProcessOptions`) — set per
  9016. // print, like `_cssEnvironment`. "none" is off; the rest name how much of the
  9017. // whitespace against a boundary no line box reaches goes with it.
  9018. /** @type {"none" | "conservative" | "smart" | "all"} */
  9019. let _collapseWhitespace = "none";
  9020. // Whether this run is minifying, for the tests that run before `printer` does and
  9021. // have to predict what it will drop.
  9022. let _minifying = false;
  9023. // The caller's renderer for source nested in this document, set per print. It
  9024. // replaces the built-in CSS/JSON minifiers and is the only way inline
  9025. // JavaScript and SVG are reached — webpack ships no minifier for either.
  9026. /** @type {EmbeddedSourceRenderer | undefined} */
  9027. let _renderEmbeddedSource;
  9028. // Where an asynchronous caller's embedded sources are recorded instead of
  9029. // rendered, set per print. A body whose text this print reads back to decide
  9030. // what to emit is still minified by the built-in for that decision — only the
  9031. // text that reaches the output is deferred.
  9032. /** @type {DeferredEmbeddedSource[] | undefined} */
  9033. let _deferEmbeddedSource;
  9034. // Whether an `<iframe srcdoc>` is among what is deferred. A caller minifying
  9035. // them itself keeps the attribute on the normal path, which re-quotes it to
  9036. // whichever delimiter its content makes shorter.
  9037. let _deferSrcdoc = true;
  9038. /**
  9039. * Hand one nested body to the caller's renderer, which must be present.
  9040. * Anything it throws is a renderer that did not answer: a document must not
  9041. * fail to serialize because something inside it did not parse.
  9042. * @param {string} source the nested body
  9043. * @param {string} type its language, e.g. `"css"` / `"javascript"` / `"svg"`
  9044. * @param {string=} as which production of that language it is, for one with more than one
  9045. * @returns {string | null} the rendered body, or null when it declined
  9046. */
  9047. const _renderEmbeddedOrNull = (source, type, as) => {
  9048. try {
  9049. const rendered = /** @type {EmbeddedSourceRenderer} */ (
  9050. _renderEmbeddedSource
  9051. )(source, {
  9052. type,
  9053. hostType: HTML_TYPE,
  9054. ...(as === undefined ? undefined : { as })
  9055. });
  9056. // Anything but text is a renderer that did not answer, not a body to write.
  9057. return typeof rendered === "string" ? rendered : null;
  9058. } catch (_err) {
  9059. return null;
  9060. }
  9061. };
  9062. /**
  9063. * Record one nested body for a caller that answers asynchronously and print the
  9064. * marker standing in for it. `onDeclined` spells what an untapped run spells,
  9065. * so declining costs nothing the built-ins would have done.
  9066. * @param {string} source the nested body
  9067. * @param {string} type its language, e.g. `"css"` / `"javascript"` / `"svg"`
  9068. * @param {(rendered: string | undefined) => string} build the text to print once the answer is in
  9069. * @param {string=} as which production of that language it is, for one with more than one
  9070. * @returns {string} the marker to print
  9071. */
  9072. const _deferEmbedded = (source, type, build, as) => {
  9073. const holes = /** @type {DeferredEmbeddedSource[]} */ (_deferEmbeddedSource);
  9074. const id = holes.length;
  9075. holes.push({
  9076. type,
  9077. hostType: HTML_TYPE,
  9078. source,
  9079. build,
  9080. ...(as === undefined ? undefined : { as })
  9081. });
  9082. return deferredWrite(id);
  9083. };
  9084. /**
  9085. * `_renderEmbeddedOrNull` for a body webpack has no minifier of its own for, so
  9086. * a renderer that declines leaves it exactly as written.
  9087. * @param {string} source the nested body
  9088. * @param {string} type its language, e.g. `"css"` / `"javascript"` / `"svg"`
  9089. * @returns {string} the rendered body, or the original
  9090. */
  9091. const _renderEmbedded = (source, type) => {
  9092. if (_deferEmbeddedSource !== undefined) {
  9093. return _deferEmbedded(source, type, (rendered) =>
  9094. typeof rendered === "string" ? rendered : source
  9095. );
  9096. }
  9097. const rendered = _renderEmbeddedOrNull(source, type);
  9098. return rendered === null ? source : rendered;
  9099. };
  9100. /**
  9101. * Whether this element is the outermost `<svg>` of an SVG subtree — the whole
  9102. * of which is offered to the renderer, so a nested one is part of its parent's.
  9103. * @param {HtmlPath} path the accessor positioned on the element
  9104. * @returns {boolean} true for an SVG root
  9105. */
  9106. const _isSvgRoot = (path) => {
  9107. if (path.tagName() !== "svg" || path.namespace() !== NS_SVG) return false;
  9108. const parent = path.parentOf();
  9109. return parent === 0 || path.namespace(parent) !== NS_SVG;
  9110. };
  9111. // Which attribute defaults may be dropped (see `HtmlProcessOptions`), off
  9112. // unless asked for: whichever tier drops one, the page stops carrying an
  9113. // attribute that `getAttribute` and every attribute selector read.
  9114. /** @type {"none" | "smart" | "all"} */
  9115. let _removeRedundantAttributes = "none";
  9116. // Whether an empty `class` / `id` / `style` / `dir` may go (see
  9117. // `HtmlProcessOptions`), off unless asked for: a presence selector reads it.
  9118. let _removeEmptyAttributes = false;
  9119. // Whether a childless, attribute-less element may go (see
  9120. // `HtmlProcessOptions`), off unless asked for: CSS can give one a size or a
  9121. // `::before`, and this cannot see the stylesheet.
  9122. let _removeEmptyElements = false;
  9123. /**
  9124. * Whether each element ends up empty in the output, memoized: the answer is
  9125. * fixed for a parse, and the walk that finds it is the whole subtree.
  9126. * @type {Map<HtmlNodeRef, boolean>}
  9127. */
  9128. const _emptyElements = new Map();
  9129. // Which comments this print keeps, with a pattern already compiled to the
  9130. // predicate it stands for — `true` every one, `false` none, `"some"` the ones
  9131. // that carry something (see `_keepComment`).
  9132. /** @type {boolean | "some" | ((comment: string) => boolean)} */
  9133. let _commentsKept = "some";
  9134. // The two reorderings (see `HtmlProcessOptions`), off unless asked for: neither
  9135. // changes what anything matches, but both change what a script reading
  9136. // `element.attributes` or `className` back sees.
  9137. let _sortAttributes = false;
  9138. let _sortTokenLists = false;
  9139. // Whether a run of adjacent `<style>` elements prints as one (see
  9140. // `HtmlProcessOptions`), off unless asked for: it removes elements, which
  9141. // `document.styleSheets` and a `:nth-child` selector read.
  9142. let _mergeStyles = false;
  9143. // A run of adjacent `<style>` elements prints as one, so the elements after the
  9144. // first print nothing at all. Filled by the first one's text as it prints, which
  9145. // is before any of theirs does; null until a document actually has a run.
  9146. /** @type {Set<HtmlElement> | null} */
  9147. let _absorbedStyles = null;
  9148. // Foster parenting puts a node before the table it was written inside, so tree
  9149. // order and source order disagree there. Recorded where it happens.
  9150. // The tree holds something no arrangement of tags reaches: an element no source
  9151. // tag wrote, or a list item a special element stopped the loop from closing.
  9152. let _printFromSource = false;
  9153. /** @type {HtmlNodeRef[] | null} this parse's fosters, flat: table, then node */
  9154. let _fosteredLog = null;
  9155. /** @type {Map<HtmlNodeRef, HtmlNodeRef[]> | null} */
  9156. let _fosteredRuns = null;
  9157. /** @type {Map<HtmlNodeRef, HtmlNodeRef> | null} */
  9158. let _fosteredNodes = null;
  9159. /** @type {Set<HtmlNodeRef> | null} */
  9160. let _fosterHolders = null;
  9161. /** @type {Map<HtmlNodeRef, boolean> | null} */
  9162. let _fosterMoves = null;
  9163. // Tables whose run has to move but cannot be replayed by re-nesting it. Their own
  9164. // source is what produced the tree, so it is what prints.
  9165. /** @type {Set<HtmlNodeRef> | null} */
  9166. let _fosterVerbatim = null;
  9167. // That holder and everything still open around it: the slice runs to where the
  9168. // source stopped, so an end tag none of them was given cannot be invented here.
  9169. /** @type {Set<HtmlNodeRef> | null} */
  9170. let _fosterOpenTail = null;
  9171. /** @type {number} how far the furthest verbatim span reached */
  9172. let _fosterSpanEnd = 0;
  9173. // How much of the `<html>` / `<head>` / `<body>` shell §13.1.2.4 lets the
  9174. // parser imply may be left out (see `HtmlProcessOptions`). Every *other*
  9175. // optional tag goes unconditionally — nothing can observe that — but these six
  9176. // are what a consumer reading the page with a regexp rather than a parser looks
  9177. // for. `"smart"` leaves out the one such a reader never matches: the `<html>`
  9178. // start tag, omittable only when it carries no attribute at all, so the
  9179. // `<html lang=en>` anyone greps for keeps its tag anyway. `</html>` stays with
  9180. // it — a truncation check reads a page as complete by finding one.
  9181. /** @type {"none" | "smart" | "all"} */
  9182. let _removeImpliedTags = "smart";
  9183. /**
  9184. * Whether the `<html>` / `<head>` / `<body>` start tag §13.1.2.4 lets the parser
  9185. * imply may be left out. A comment minification keeps, whitespace, or an element
  9186. * the parser still reads as head content would move above the tag once it goes;
  9187. * a comment it drops is gone from the output, so it is skipped over.
  9188. * @param {HtmlPath} path the accessor
  9189. * @param {string} name the element's lowercased tag name
  9190. * @param {HtmlNodeRef} node the element
  9191. * @returns {boolean} true when the start tag can be dropped
  9192. */
  9193. const _canOmitShellStart = (path, name, node) => {
  9194. if (name !== "html" && name !== "head" && name !== "body") return false;
  9195. if (path.attributeCount(node) !== 0) return false;
  9196. let first = path.firstChild(node);
  9197. while (first !== 0 && _printsNothing(path, first)) {
  9198. first = path.nextSibling(first);
  9199. }
  9200. if (first === 0) return name === "html" || _removeImpliedTags === "all";
  9201. const type = path.type(first);
  9202. if (type === NodeType.Comment) return false;
  9203. if (name === "html") return true;
  9204. if (_removeImpliedTags !== "all") return false;
  9205. if (name === "head") return type === NodeType.Element;
  9206. if (type === NodeType.Text) return !startsWithWs(path.data(first));
  9207. return !BODY_START_KEPT_BEFORE.has(path.tagName(first));
  9208. };
  9209. /**
  9210. * Whether `</html>` / `</head>` / `</body>` can be left out without the parser
  9211. * handing what follows to the element it closed: §4.13 lets an `html` or `body`
  9212. * end tag go when a comment does not follow it, and a `head` one when neither a
  9213. * comment nor whitespace does.
  9214. * @param {HtmlPath} path the accessor
  9215. * @param {string} name the element's lowercased tag name
  9216. * @param {HtmlNodeRef} node the element
  9217. * @returns {boolean} true when nothing behind the tag would move inside
  9218. */
  9219. const _shellEndTagOmittable = (path, name, node) => {
  9220. let next = path.nextSibling(node);
  9221. while (next !== 0 && _printsNothing(path, next)) {
  9222. next = path.nextSibling(next);
  9223. }
  9224. if (next === 0) return true;
  9225. const type = path.type(next);
  9226. if (type === NodeType.Comment) return false;
  9227. return (
  9228. name !== "head" || type !== NodeType.Text || !startsWithWs(path.data(next))
  9229. );
  9230. };
  9231. /**
  9232. * The same, for the end tag — `"all"` only: `"smart"` keeps every one of them,
  9233. * `</html>` included, because a reader checking a page downloaded whole looks
  9234. * for it. A comment behind the tag would move inside the element it closed,
  9235. * and whitespace behind `</head>` into `<body>`.
  9236. * @param {HtmlPath} path the accessor
  9237. * @param {string} name the element's lowercased tag name
  9238. * @param {HtmlNodeRef} node the element
  9239. * @returns {boolean} true when the end tag can be dropped
  9240. */
  9241. const _canOmitShellEnd = (path, name, node) =>
  9242. _removeImpliedTags === "all" && _shellEndTagOmittable(path, name, node);
  9243. /**
  9244. * Whether a parser-inserted table group has to print a start tag the source
  9245. * never spelled. With `removeOptionalTags` off its end tag prints, and a
  9246. * `</tbody>` with no `<tbody>` is not the document that was parsed.
  9247. * @param {HtmlPath} path the accessor positioned on the element
  9248. * @param {string} name its lowercased tag name
  9249. * @param {HtmlNodeRef} node the element
  9250. * @returns {boolean} true when the start tag has to be materialized
  9251. */
  9252. const _impliedTableGroupStartNeeded = (path, name, node) => {
  9253. if (!_transforms.removeOptionalTags) {
  9254. return name === "tbody" || name === "colgroup";
  9255. }
  9256. // §4.13 lets the start tag go only when the group before it kept its end
  9257. // tag, or that one would take these columns over.
  9258. return name === "colgroup" && !_canOmitColgroupStart(path, node);
  9259. };
  9260. /**
  9261. * Whether an element's start tag may be left out: one the parser re-implies, or
  9262. * a shell tag the option lets go. A shell element the source never spelled falls
  9263. * through to the transparent-implied path, which already prints nothing for it.
  9264. * @param {HtmlPath} path the accessor positioned on the element
  9265. * @param {string} name its lowercased tag name
  9266. * @param {HtmlNodeRef} node the element
  9267. * @param {string} open what its start tag printed as, `""` when the source
  9268. * spelled none
  9269. * @returns {boolean} true when the start tag can be dropped
  9270. */
  9271. const _canOmitStartTag = (path, name, node, open) => {
  9272. if (name === "tbody") {
  9273. return _transforms.removeOptionalTags && _canOmitTbodyStart(path, node);
  9274. }
  9275. if (name === "colgroup") {
  9276. return _transforms.removeOptionalTags && _canOmitColgroupStart(path, node);
  9277. }
  9278. return (
  9279. open !== "" &&
  9280. _removeImpliedTags !== "none" &&
  9281. _canOmitShellStart(path, name, node)
  9282. );
  9283. };
  9284. /**
  9285. * Minify a `<style>` body with webpack's own CSS minifier — the one that runs on
  9286. * CSS assets, so an inline sheet is held to the same rules. `null` when it
  9287. * declines the text, which a caller joining two sheets has to tell apart from a
  9288. * sheet that was already minimal: text the minifier cannot parse may be
  9289. * unterminated, and then it swallows whatever is concatenated after it.
  9290. * @param {string} css the element's text
  9291. * @param {{ text: string }=} decided filled with the text this print may read back, which is the built-in's rather than the marker when the caller's renderer is deferred
  9292. * @returns {string | null} the minified text, or null
  9293. */
  9294. const _minifyStyleBody = (css, decided) => {
  9295. if (!_transforms.minifyStyles) return null;
  9296. if (css.trim() === "") return css;
  9297. try {
  9298. if (_deferEmbeddedSource !== undefined) {
  9299. // The built-in still runs: what this print does next is decided by
  9300. // reading the minified text back (whether a run merges), and a marker
  9301. // cannot be read. Only the text that reaches the output is the caller's,
  9302. // and `decided` carries the readable text to whoever needs it.
  9303. const builtin = _builtinStyleBody(css);
  9304. if (builtin === null) return null;
  9305. if (decided !== undefined) decided.text = builtin;
  9306. return _deferEmbedded(css, CSS_TYPE, (rendered) =>
  9307. typeof rendered === "string" ? rendered : builtin
  9308. );
  9309. }
  9310. if (_renderEmbeddedSource !== undefined) {
  9311. // Declining falls back to the built-in, so a renderer that covers some
  9312. // languages does not turn the ones it does not cover off.
  9313. const rendered = _renderEmbeddedOrNull(css, CSS_TYPE);
  9314. if (rendered !== null) return rendered;
  9315. }
  9316. return _builtinStyleBody(css);
  9317. } catch (_err) {
  9318. return null;
  9319. }
  9320. };
  9321. /**
  9322. * The CSS minify webpack ships, for a body no caller's renderer answered for.
  9323. * @param {string} css the element's text
  9324. * @param {("stylesheet" | "block-contents")=} as which production it is — a `style=""` holds a block's contents, an element a stylesheet
  9325. * @returns {string | null} the minified text, or null
  9326. */
  9327. const _builtinStyleBody = (css, as) => {
  9328. try {
  9329. const cssSyntax = _cssSyntax || (_cssSyntax = require("../css/syntax"));
  9330. const options = {
  9331. environment: _cssEnvironment,
  9332. convertLengthUnits: _cssConvertLengthUnits,
  9333. rewriteCustomProperties: _cssRewriteCustomProperties,
  9334. transforms: _cssTransforms
  9335. };
  9336. return new cssSyntax.SourceProcessor().process(css, {
  9337. mode: "minify",
  9338. ...options,
  9339. as
  9340. }).code;
  9341. } catch (_err) {
  9342. return null;
  9343. }
  9344. };
  9345. /**
  9346. * `_minifyStyleBody`, with text it rejects handed back untouched rather than
  9347. * failing the build.
  9348. * @param {string} css the element's text
  9349. * @returns {string} the minified text
  9350. */
  9351. const _minifyInlineCss = (css) => {
  9352. const minified = _minifyStyleBody(css);
  9353. return minified === null ? css : minified;
  9354. };
  9355. // The throwaway rule a `style` attribute's declaration list is minified inside.
  9356. // The selector is only ever written and read back here, so it is the shortest
  9357. // one the printer cannot rewrite — and the two halves are named so the read back
  9358. // out cannot drift from what was written in.
  9359. /**
  9360. * How one attribute is written out, and whether it leaves the tag ending in an
  9361. * unquoted value — the shortest of the minimal reference spelling, the value
  9362. * bare, and the value quoted. `null` when none of them beats the source.
  9363. * @param {string} attributeName the attribute's name
  9364. * @param {string} value its rewritten value
  9365. * @param {string} rawValue its value as the source spelled it
  9366. * @param {string | null} decoded the value decoded, for one carrying character references, or null
  9367. * @returns {{ text: string, unquotedTail: boolean } | null} what to emit, or null to leave the source alone
  9368. */
  9369. const _writeAttribute = (attributeName, value, rawValue, decoded) => {
  9370. const minimal =
  9371. _transforms.normalizeAttributeQuotes && decoded !== null
  9372. ? _minimalAttributeValue(decoded)
  9373. : null;
  9374. const unquotable =
  9375. _transforms.normalizeAttributeQuotes && _canUnquoteAttributeValue(value);
  9376. if (
  9377. minimal !== null &&
  9378. minimal.length < (unquotable ? value.length : rawValue.length + 2)
  9379. ) {
  9380. const opened = minimal.charCodeAt(0);
  9381. return {
  9382. text: ` ${attributeName}=${minimal}`,
  9383. unquotedTail: opened !== CC_QUOTATION_MARK && opened !== CC_APOSTROPHE
  9384. };
  9385. }
  9386. if (unquotable) {
  9387. return { text: ` ${attributeName}=${value}`, unquotedTail: true };
  9388. }
  9389. if (value !== rawValue) {
  9390. const quotedValue = _quoteAttributeValue(value);
  9391. if (quotedValue !== null) {
  9392. return { text: ` ${attributeName}=${quotedValue}`, unquotedTail: false };
  9393. }
  9394. }
  9395. return null;
  9396. };
  9397. /**
  9398. * Minify a `style` attribute's declaration list with webpack's CSS minifier, as
  9399. * the block-contents production it is — which is what gives the list the
  9400. * duplicate-drop and shorthand merges a rule's block gets.
  9401. * @param {string} raw raw (undecoded) attribute value
  9402. * @returns {string} the minified declarations
  9403. */
  9404. const _minifyStyleAttribute = (raw) => {
  9405. // Ahead of the memo: with the switch off nothing is minified, so there is
  9406. // nothing to keep and nothing to read back.
  9407. if (!_transforms.minifyStyles) return raw;
  9408. // A caller's renderer is handed every `style=""` the document holds, so it is
  9409. // not a function of the value alone and nothing about it may be reused.
  9410. if (_renderEmbeddedSource !== undefined) {
  9411. return _minifyStyleAttributeUncached(raw);
  9412. }
  9413. const memoized = _styleAttributeCache.get(raw);
  9414. if (memoized !== undefined) return memoized;
  9415. const minified = _minifyStyleAttributeUncached(raw);
  9416. _styleAttributeCache.set(raw, minified);
  9417. return minified;
  9418. };
  9419. /**
  9420. * `_minifyStyleAttribute` without the memo — every path here runs the CSS
  9421. * minifier, and every return is safe to keep, none of them reading state the
  9422. * print goes on to change.
  9423. * @param {string} raw raw (undecoded) attribute value
  9424. * @returns {string} the minified declarations
  9425. */
  9426. const _minifyStyleAttributeUncached = (raw) => {
  9427. if (raw.trim() === "") return raw;
  9428. // Offered as the CSS it is, with `as` saying which production — the wrapper a
  9429. // declaration list needs for the transforms a block's contents get is the CSS
  9430. // layer's own business. The deferring path offers it at the attribute
  9431. // instead, where the shape an answer has to land in is known.
  9432. if (
  9433. _renderEmbeddedSource !== undefined &&
  9434. _deferEmbeddedSource === undefined
  9435. ) {
  9436. const rendered = _renderEmbeddedOrNull(raw, CSS_TYPE, BLOCK_CONTENTS);
  9437. if (rendered !== null) return rendered;
  9438. }
  9439. const out = _builtinStyleBody(raw, BLOCK_CONTENTS);
  9440. return out === null ? raw : out;
  9441. };
  9442. /**
  9443. * Trim ASCII whitespace, which is all these grammars skip — `String#trim` also
  9444. * eats NBSP and the other Unicode spaces, which they read as ordinary
  9445. * characters (a leading NBSP makes an integer unparsable, and is a class token
  9446. * of its own).
  9447. * @param {string} value the value
  9448. * @returns {string} the trimmed value
  9449. */
  9450. const _asciiTrim = (value) => {
  9451. let start = 0;
  9452. let end = value.length;
  9453. while (start < end && isSpace(value.charCodeAt(start))) start++;
  9454. while (end > start && isSpace(value.charCodeAt(end - 1))) end--;
  9455. return start === 0 && end === value.length ? value : value.slice(start, end);
  9456. };
  9457. /**
  9458. * Normalize a `<meta name=viewport>` content list: its grammar ignores the
  9459. * whitespace around the `,` / `;` / `=` separators, so dropping that keeps every
  9460. * pair intact.
  9461. * @param {string} raw raw (undecoded) attribute value
  9462. * @returns {string} the normalized list
  9463. */
  9464. const _normalizeViewport = (raw) =>
  9465. _asciiTrim(raw)
  9466. .replace(/[\t\n\f\r ]*([,;=])[\t\n\f\r ]*/g, "$1")
  9467. .replace(/[\t\n\f\r ]+/g, " ");
  9468. /**
  9469. * Wrap a rewritten attribute value in the shortest quote it admits, or null when
  9470. * it carries both kinds and the source spelling has to stand.
  9471. * @param {string} value the value
  9472. * @returns {string | null} the quoted value, or null
  9473. */
  9474. const _quoteAttributeValue = (value) => {
  9475. if (!value.includes('"')) return `"${value}"`;
  9476. if (!value.includes("'")) return `'${value}'`;
  9477. return null;
  9478. };
  9479. /**
  9480. * The shortest way to write a decoded value: bare where the grammar allows it,
  9481. * else under whichever quote costs less. `<`, `>` and the quote that is not the
  9482. * delimiter carry no meaning inside a quoted value, so `alt="say &quot;hi&quot;"`
  9483. * is `alt='say "hi"'` and `title="&lt;b&gt;"` is `title="<b>"`.
  9484. * @param {string} value the decoded attribute value
  9485. * @returns {string} the value as written, delimiters included
  9486. */
  9487. const _minimalAttributeValue = (value) => {
  9488. const underDouble = escapeAttribute(value, CC_QUOTATION_MARK, true);
  9489. const underSingle = escapeAttribute(value, CC_APOSTROPHE, true);
  9490. let best = `"${underDouble}"`;
  9491. if (underSingle.length + 2 < best.length) best = `'${underSingle}'`;
  9492. // A bare value costs no delimiters, so it wins whenever it is legal at all.
  9493. for (const bare of [underDouble, underSingle]) {
  9494. if (bare.length < best.length && _canUnquoteAttributeValue(bare)) {
  9495. best = bare;
  9496. }
  9497. }
  9498. return best;
  9499. };
  9500. /**
  9501. * Whether an attribute only restates the element's own default, so dropping it
  9502. * leaves the DOM reading the same value — but leaves an attribute selector,
  9503. * which matches the content attribute, with nothing to match. Both tables are
  9504. * looked up per element by the caller, not per attribute here: the answer is the
  9505. * same for every attribute of one tag.
  9506. * @param {Record<string, string> | undefined} markers the element's marker table
  9507. * @param {Record<string, string> | undefined} defaults the element's spec-default table
  9508. * @param {string} tagName lowercased element name
  9509. * @param {string} name lowercased attribute name
  9510. * @param {string} value the attribute's raw value
  9511. * @returns {boolean} true when the attribute may be dropped
  9512. */
  9513. const _isRedundantAttribute = (markers, defaults, tagName, name, value) => {
  9514. const lowered = _asciiLowerCase(_asciiTrim(value));
  9515. // `<script type>`: only a spelling that names JavaScript, which is what the
  9516. // element already runs. `module` and the empty spelling say something.
  9517. if (tagName === "script" && name === "type") {
  9518. return (
  9519. lowered !== "" &&
  9520. lowered !== "module" &&
  9521. JAVASCRIPT_SCRIPT_TYPES.has(lowered)
  9522. );
  9523. }
  9524. if (markers !== undefined && markers[name] === lowered) return true;
  9525. return defaults !== undefined && defaults[name] === lowered;
  9526. };
  9527. /**
  9528. * Whether an empty attribute reads as its own absence, so `removeEmptyAttributes`
  9529. * may drop it — only on the elements the spec defines it on, never `<x-foo rel="">`.
  9530. * @param {string} elementName lowercased element name
  9531. * @param {string} name lowercased attribute name
  9532. * @param {string} value the attribute's raw value
  9533. * @returns {boolean} true when the attribute may be dropped
  9534. */
  9535. const _isRemovableEmptyAttribute = (elementName, name, value) => {
  9536. if (!_removeEmptyAttributes) return false;
  9537. const on = EMPTY_REMOVABLE_ATTRIBUTES.get(name);
  9538. if (on === undefined || (on !== null && !on.has(elementName))) return false;
  9539. // Read raw, so a value spelled with a character reference is kept whatever it
  9540. // decodes to. `_asciiTrim` returns the input when there is nothing to trim.
  9541. return _asciiTrim(value) === "";
  9542. };
  9543. /**
  9544. * Whether an element carries nothing at all, so `removeEmptyElements` may drop
  9545. * it. Every guard but the name table is a rule: a void element is childless by
  9546. * definition, a `<template>`'s children hang off a content fragment, foreign
  9547. * content is not ours to judge, and an element spelling any attribute was
  9548. * written for a reason — which is what keeps `<script src>`, `<iframe src>` and
  9549. * `<div id=mount>`, each of which html-minifier-terser drops.
  9550. *
  9551. * What no guard can cover is CSS: `.spacer { height: 4px }` or a `::before`
  9552. * makes an empty element visible, and this cannot see the stylesheet. That is
  9553. * the risk the option is off by default for.
  9554. * @param {HtmlPath} path the accessor positioned on the element
  9555. * @param {HtmlElement} element the element
  9556. * @returns {boolean} true when the element may be dropped
  9557. */
  9558. // Every test is pure, so the O(1) ones come first: `_childrenPrintNothing`
  9559. // walks the subtree, and the printer asks this of every element. Memoized for
  9560. // the same reason — without it a nested run costs one walk per ancestor.
  9561. const _isRemovableEmptyElement = (path, element) => {
  9562. if (!_removeEmptyElements) return false;
  9563. // Asked before the memo: on component markup nearly every element answers
  9564. // here, and the count costs less than the memo round trip.
  9565. if (path.attributeCount(element) !== 0) return false;
  9566. const memo = _emptyElements.get(element);
  9567. if (memo !== undefined) return memo;
  9568. const answer =
  9569. path.templateContent(element) === 0 &&
  9570. path.namespace(element) === NS_HTML &&
  9571. // Void is otherwise disqualifying — a `<br>` is childless and still
  9572. // renders. A void element that belongs in the head is the exception: with
  9573. // no attributes it states nothing.
  9574. (!VOID.has(path.tagName(element)) ||
  9575. EMPTY_METADATA_ELEMENTS.has(path.tagName(element))) &&
  9576. !EMPTY_ELEMENT_KEPT.has(path.tagName(element)) &&
  9577. _childrenPrintNothing(path, element);
  9578. _emptyElements.set(element, answer);
  9579. return answer;
  9580. };
  9581. /**
  9582. * Whether every child of an element prints nothing, so the element ends up
  9583. * empty in the output whatever the source tree holds. Recursive with
  9584. * `_isRemovableEmptyElement`, which is what lets a run of nested empties go
  9585. * together rather than one layer per build.
  9586. * @param {HtmlPath} path the accessor
  9587. * @param {HtmlElement} element the element
  9588. * @returns {boolean} true when nothing inside it reaches the output
  9589. */
  9590. const _childrenPrintNothing = (path, element) => {
  9591. for (
  9592. let child = _nodeFirstChildren[element];
  9593. child !== 0;
  9594. child = path.nextSibling(child)
  9595. ) {
  9596. if (
  9597. path.type(child) === NodeType.Element
  9598. ? !_isRemovableEmptyElement(path, /** @type {HtmlElement} */ (child))
  9599. : !_printsNothing(path, child)
  9600. ) {
  9601. return false;
  9602. }
  9603. }
  9604. return true;
  9605. };
  9606. /**
  9607. * Whether an attribute is boolean on this element, so its presence alone is the
  9608. * value. `null` in the table means the attribute is global.
  9609. * @param {string} tagName lowercased element name
  9610. * @param {string} name lowercased attribute name
  9611. * @returns {boolean} true when the value carries nothing
  9612. */
  9613. const _isBooleanAttribute = (tagName, name) => {
  9614. const elements = BOOLEAN_ATTRIBUTES.get(name);
  9615. if (elements === undefined) return BOOLEAN_ATTRIBUTES.has(name);
  9616. return elements === null || elements.has(tagName);
  9617. };
  9618. /**
  9619. * Whether a space-separated list names the same token twice, read span by span
  9620. * so nothing is allocated. The lists are short — `class` is a handful of tokens
  9621. * — so comparing each pair beats building a set of them.
  9622. * @param {string} list a collapsed, trimmed token list
  9623. * @returns {boolean} true when some token appears more than once
  9624. */
  9625. const _repeatsAToken = (list) => {
  9626. const length = list.length;
  9627. for (let start = 0; start < length;) {
  9628. let end = list.indexOf(" ", start);
  9629. if (end === -1) end = length;
  9630. const width = end - start;
  9631. for (let other = end + 1; other < length;) {
  9632. let otherEnd = list.indexOf(" ", other);
  9633. if (otherEnd === -1) otherEnd = length;
  9634. if (otherEnd - other === width) {
  9635. let i = 0;
  9636. while (
  9637. i < width &&
  9638. list.charCodeAt(start + i) === list.charCodeAt(other + i)
  9639. ) {
  9640. i++;
  9641. }
  9642. if (i === width) return true;
  9643. }
  9644. other = otherEnd + 1;
  9645. }
  9646. start = end + 1;
  9647. }
  9648. return false;
  9649. };
  9650. /**
  9651. * Collapse an ASCII-whitespace-separated token list (`class`) to single spaces,
  9652. * and — where the DOM reflects the list as a `DOMTokenList`, an ordered *set*,
  9653. * so a repeat was never a second token — drop a token already in it. Order is
  9654. * kept either way: the first spelling of each token stays put.
  9655. * @param {string} raw raw (undecoded) value
  9656. * @param {boolean} unique whether a repeated token may go (`DOM_TOKEN_LIST_ATTRIBUTES`)
  9657. * @param {boolean} sort whether to print the tokens in order (`sortTokenLists`)
  9658. * @returns {string} the collapsed list
  9659. */
  9660. const _normalizeTokenList = (raw, unique, sort) => {
  9661. // One walk answers "already collapsed?" — the common already-minified list
  9662. // rewrites nothing, so it must not pay the trim + regex replace either.
  9663. let dirty = false;
  9664. let hasSpace = false;
  9665. let previousSpace = true;
  9666. for (let i = 0; i < raw.length; i++) {
  9667. const c = raw.charCodeAt(i);
  9668. if (
  9669. (c >= 0x09 && c <= 0x0d && c !== 0x0b) ||
  9670. (c === 0x20 && previousSpace)
  9671. ) {
  9672. dirty = true;
  9673. break;
  9674. }
  9675. if (c === 0x20) {
  9676. hasSpace = true;
  9677. previousSpace = true;
  9678. } else {
  9679. previousSpace = false;
  9680. }
  9681. }
  9682. if (!dirty && previousSpace && raw.length !== 0) dirty = true;
  9683. const collapsed = dirty ? _asciiTrim(raw).replace(/[\t\n\f\r ]+/g, " ") : raw;
  9684. if (dirty ? !collapsed.includes(" ") : !hasSpace) return collapsed;
  9685. // Cutting the list into tokens is the allocation this runs often enough to
  9686. // care about, and almost no list repeats one — so the check reads the spans
  9687. // in place and only a list that does pay for the pieces. Sorting needs them
  9688. // either way.
  9689. if (!sort && !(unique && _repeatsAToken(collapsed))) return collapsed;
  9690. if (sort) return _sortedTokenList(collapsed, unique);
  9691. return [...new Set(collapsed.split(" "))].join(" ");
  9692. };
  9693. // One list's token spans, reused: ordering a list allocates only its result.
  9694. /** @type {number[]} */
  9695. const _tokenSpanStarts = [];
  9696. /** @type {number[]} */
  9697. const _tokenSpanEnds = [];
  9698. /**
  9699. * Compare two of `list`'s tokens where they sit, in the code unit order
  9700. * `Array#sort` puts strings in — a prefix sorts before what extends it.
  9701. * @param {string} list the collapsed list
  9702. * @param {number} aStart first token's start
  9703. * @param {number} aEnd first token's end
  9704. * @param {number} bStart second token's start
  9705. * @param {number} bEnd second token's end
  9706. * @returns {number} negative, zero or positive
  9707. */
  9708. const _compareTokens = (list, aStart, aEnd, bStart, bEnd) => {
  9709. const shared = Math.min(aEnd - aStart, bEnd - bStart);
  9710. for (let i = 0; i < shared; i++) {
  9711. const difference =
  9712. list.charCodeAt(aStart + i) - list.charCodeAt(bStart + i);
  9713. if (difference !== 0) return difference;
  9714. }
  9715. return aEnd - aStart - (bEnd - bStart);
  9716. };
  9717. /**
  9718. * `collapsed`'s tokens in sorted order, deduplicated when it is a set. They are
  9719. * ordered where they sit, so the pieces `split` would build are never built.
  9720. * @param {string} collapsed a single-space-separated list
  9721. * @param {boolean} unique whether repeats collapse
  9722. * @returns {string} the ordered list
  9723. */
  9724. const _sortedTokenList = (collapsed, unique) => {
  9725. const starts = _tokenSpanStarts;
  9726. const ends = _tokenSpanEnds;
  9727. let count = 0;
  9728. for (let at = 0; ;) {
  9729. const space = collapsed.indexOf(" ", at);
  9730. starts[count] = at;
  9731. ends[count] = space === -1 ? collapsed.length : space;
  9732. count++;
  9733. if (space === -1) break;
  9734. at = space + 1;
  9735. }
  9736. // A list carries a handful of tokens, so insertion sort beats `Array#sort`'s
  9737. // setup and leaves equal tokens adjacent for the pass below to drop.
  9738. for (let i = 1; i < count; i++) {
  9739. const start = starts[i];
  9740. const end = ends[i];
  9741. let j = i - 1;
  9742. while (
  9743. j >= 0 &&
  9744. _compareTokens(collapsed, starts[j], ends[j], start, end) > 0
  9745. ) {
  9746. starts[j + 1] = starts[j];
  9747. ends[j + 1] = ends[j];
  9748. j--;
  9749. }
  9750. starts[j + 1] = start;
  9751. ends[j + 1] = end;
  9752. }
  9753. let out = "";
  9754. for (let i = 0; i < count; i++) {
  9755. if (
  9756. unique &&
  9757. i !== 0 &&
  9758. _compareTokens(
  9759. collapsed,
  9760. starts[i - 1],
  9761. ends[i - 1],
  9762. starts[i],
  9763. ends[i]
  9764. ) === 0
  9765. ) {
  9766. continue;
  9767. }
  9768. if (out !== "") out += " ";
  9769. out += collapsed.slice(starts[i], ends[i]);
  9770. }
  9771. return out;
  9772. };
  9773. /**
  9774. * Drop the whitespace a comma-separated list's grammar ignores: "split a string
  9775. * on commas" strips each token's edges and nothing else. Whitespace inside a
  9776. * token stays — it is part of the token `accept` keeps, and a separator of its
  9777. * own in the number list `coords` parses.
  9778. * @param {string} raw raw (undecoded) value
  9779. * @returns {string} the normalized list
  9780. */
  9781. const _normalizeCommaList = (raw) =>
  9782. raw
  9783. .split(",")
  9784. .map((item) => _asciiTrim(item))
  9785. .join(",");
  9786. /**
  9787. * Normalize an integer attribute: the parse rules skip leading ASCII whitespace
  9788. * and stop at the first non-digit, so the ends and any leading zeros carry
  9789. * nothing. A value those rules would not read as one integer is left alone —
  9790. * only the signed rules accept a `+` or `-`, so a signed value on a
  9791. * non-negative attribute stays as it is (it does not parse at all).
  9792. * @param {string} raw raw (undecoded) value
  9793. * @param {boolean} signed whether the attribute parses with the signed rules
  9794. * @returns {string} the normalized integer
  9795. */
  9796. const _normalizeInteger = (raw, signed) => {
  9797. const value = _asciiTrim(raw);
  9798. if (!(signed ? /^[+-]?\d+$/ : /^\d+$/).test(value)) return raw;
  9799. const negative = value.charCodeAt(0) === 0x2d;
  9800. const digits = value.replace(/^[+-]/, "").replace(/^0+(?=\d)/, "");
  9801. return negative && digits !== "0" ? `-${digits}` : digits;
  9802. };
  9803. // The `*` bucket, read on every attribute the element does not scope itself.
  9804. const ENUMERATED_GLOBAL_KEYWORDS = ENUMERATED_KEYWORDS["*"];
  9805. /**
  9806. * The value rewrites that keep an attribute's parsed meaning: a srcset's
  9807. * whitespace, a `style` declaration list, and the viewport `content` list.
  9808. * @param {string} element lowercased element name
  9809. * @param {string} name lowercased attribute name
  9810. * @param {string} raw raw (undecoded) value
  9811. * @param {boolean} viewport whether the element is `<meta name=viewport>`
  9812. * @param {Record<string, Set<string>> | undefined} enumeratedOn what the element scopes itself, looked up once per element by the caller
  9813. * @returns {string} the rewritten value, or `raw` when nothing applies
  9814. */
  9815. const _rewriteAttributeValue = (element, name, raw, viewport, enumeratedOn) => {
  9816. // One question for the whole chain below, and most attributes on a page — a
  9817. // `data-*`, an `id`, an `aria-*` — are done here rather than after a miss in
  9818. // each table in turn.
  9819. if (!REWRITABLE_ATTRIBUTES.has(name)) return raw;
  9820. // An enumerated value matches ASCII-case-insensitively, so folding one the
  9821. // spec names changes nothing the DOM reads.
  9822. if (
  9823. _transforms.normalizeEnumeratedAttributes &&
  9824. ENUMERATED_ATTRIBUTE_NAMES.has(name)
  9825. ) {
  9826. const own = enumeratedOn === undefined ? undefined : enumeratedOn[name];
  9827. const enumerated =
  9828. own === undefined ? ENUMERATED_GLOBAL_KEYWORDS[name] : own;
  9829. if (enumerated !== undefined) {
  9830. // Case-insensitively, and nothing else: an enumerated attribute names a
  9831. // keyword, so `type="time "` is not `time` — it is the value no keyword
  9832. // matches, which puts the input in its default state.
  9833. const lowered = _asciiLowerCase(raw);
  9834. // A value the spec does not enumerate is left exactly as written:
  9835. // `target` names a browsing context, and `type` on a custom element
  9836. // means what it says.
  9837. if (enumerated.has(lowered)) return lowered;
  9838. }
  9839. }
  9840. if (SRCSET_ATTRIBUTES.has(name)) {
  9841. return _transforms.normalizeListAttributes ? _normalizeSrcset(raw) : raw;
  9842. }
  9843. if (name === "style") return _minifyStyleAttribute(raw);
  9844. const tokenListOn = _transforms.normalizeListAttributes
  9845. ? TOKEN_LIST_ATTRIBUTES.get(name)
  9846. : undefined;
  9847. if (
  9848. tokenListOn !== undefined &&
  9849. (tokenListOn === null || tokenListOn.has(element))
  9850. ) {
  9851. // Only the lists the DOM reflects as a `DOMTokenList` are sets, so only
  9852. // they may be reordered — `ping` is the order its requests go out in and
  9853. // `accesskey` the order its keys are tried.
  9854. const isSet = DOM_TOKEN_LIST_ATTRIBUTES.has(name);
  9855. return _normalizeTokenList(raw, isSet, _sortTokenLists && isSet);
  9856. }
  9857. if (_transforms.normalizeListAttributes) {
  9858. if (viewport && name === "content") return _normalizeViewport(raw);
  9859. if (COMMA_LIST_ATTRIBUTES.has(name)) return _normalizeCommaList(raw);
  9860. }
  9861. const urlOn = URL_ATTRIBUTES.get(name);
  9862. if (urlOn !== undefined && (urlOn === null || urlOn.has(element))) {
  9863. return _asciiTrim(raw);
  9864. }
  9865. const integerOn = _transforms.normalizeNumericAttributes
  9866. ? INTEGER_ATTRIBUTES.get(name)
  9867. : undefined;
  9868. if (
  9869. integerOn !== undefined &&
  9870. (integerOn === null || integerOn.has(element))
  9871. ) {
  9872. return _normalizeInteger(raw, SIGNED_INTEGER_ATTRIBUTES.has(name));
  9873. }
  9874. return raw;
  9875. };
  9876. /**
  9877. * A `<script>`'s lowercased `type`, or `""` when it has none.
  9878. * @param {HtmlPath} path the accessor
  9879. * @param {HtmlElement} element the `<script>` element
  9880. * @returns {string} the type
  9881. */
  9882. const _scriptType = (path, element) => {
  9883. for (const attribute of path.attributes(element)) {
  9884. if (attribute.name === "type") {
  9885. return _asciiLowerCase(_asciiTrim(attribute.value));
  9886. }
  9887. }
  9888. return "";
  9889. };
  9890. /**
  9891. * Whether an element is the viewport `<meta>`, whose content list is the only
  9892. * `content` this rewrites.
  9893. * @param {HtmlPath} path the accessor positioned on the `<meta>`
  9894. * @returns {boolean} true for `<meta name=viewport>`
  9895. */
  9896. const _isViewportMeta = (path) => {
  9897. for (const attribute of path.attributes()) {
  9898. if (attribute.name === "name") {
  9899. return _asciiLowerCase(_asciiTrim(attribute.value)) === "viewport";
  9900. }
  9901. }
  9902. return false;
  9903. };
  9904. const _JSON_SUBTYPE_REGEXP =
  9905. /^[!#$%&'*+.^_`|~\w-]+\/[!#$%&'*+.^_`|~\w-]*\+json$/;
  9906. /**
  9907. * Strip the whitespace between a JSON body's tokens. Every literal is copied
  9908. * byte for byte — re-serializing would reorder nothing but would round numbers
  9909. * through a double, drop a duplicate key and rewrite escapes, none of which is
  9910. * this transform's to do. Escapes are also the security case: unescaping a
  9911. * `<` lets a string close the `<script>` early.
  9912. * @param {string} json a `<script>` body
  9913. * @returns {string} the stripped body
  9914. */
  9915. const _minifyInlineJson = (json) => {
  9916. if (!_transforms.minifyJson) return json;
  9917. if (json.trim() === "") return json;
  9918. if (_deferEmbeddedSource !== undefined) {
  9919. return _deferEmbedded(json, JSON_TYPE, (rendered) =>
  9920. typeof rendered === "string" ? rendered : _builtinInlineJson(json)
  9921. );
  9922. }
  9923. if (_renderEmbeddedSource !== undefined) {
  9924. const rendered = _renderEmbeddedOrNull(json, JSON_TYPE);
  9925. if (rendered !== null) return rendered;
  9926. }
  9927. return _builtinInlineJson(json);
  9928. };
  9929. /**
  9930. * The JSON strip webpack ships, for a body no caller's renderer answered for.
  9931. * @param {string} json a `<script>` body
  9932. * @returns {string} the stripped body
  9933. */
  9934. const _builtinInlineJson = (json) => {
  9935. try {
  9936. JSON.parse(json);
  9937. } catch (_err) {
  9938. // Not JSON after all (a template, a placeholder) — not ours to touch.
  9939. return json;
  9940. }
  9941. let out = "";
  9942. let inString = false;
  9943. let escaped = false;
  9944. for (let i = 0; i < json.length; i++) {
  9945. const c = json.charCodeAt(i);
  9946. if (inString) {
  9947. out += json[i];
  9948. if (escaped) escaped = false;
  9949. else if (c === 0x5c) escaped = true;
  9950. else if (c === 0x22) inString = false;
  9951. continue;
  9952. }
  9953. if (c === 0x22) {
  9954. inString = true;
  9955. out += json[i];
  9956. continue;
  9957. }
  9958. // JSON whitespace (RFC 8259): tab, LF, CR, space.
  9959. if (c === 0x09 || c === 0x0a || c === 0x0d || c === 0x20) continue;
  9960. out += json[i];
  9961. }
  9962. return out;
  9963. };
  9964. /**
  9965. * Whether a `<style>` element's body is CSS: the attribute is optional, and the
  9966. * only value that keeps it CSS is `text/css`.
  9967. * @param {HtmlPath} path the accessor
  9968. * @param {HtmlElement} element the `<style>` element
  9969. * @returns {boolean} true when the body may be minified as CSS
  9970. */
  9971. const _isCssStyleElement = (path, element) => {
  9972. for (const attribute of path.attributes(element)) {
  9973. if (attribute.name !== "type") continue;
  9974. const value = _asciiLowerCase(_asciiTrim(attribute.value));
  9975. return value === "" || value === "text/css";
  9976. }
  9977. return true;
  9978. };
  9979. // An at-rule that only applies at the top of a sheet, so a second sheet carrying
  9980. // one cannot be appended to a first.
  9981. const _SHEET_LEADING_AT_RULE_REGEXP = /@(?:charset|import|namespace)\b/i;
  9982. /**
  9983. * A `<style>`'s only child when that is one text node — the shape a merge needs,
  9984. * since `<style>` is parsed as raw text and anything else means it is empty.
  9985. * @param {HtmlElement} element the `<style>` element
  9986. * @returns {HtmlNodeRef} the text node, or 0
  9987. */
  9988. const _styleTextChild = (element) => {
  9989. const first = _nodeFirstChildren[element];
  9990. return first !== 0 &&
  9991. _nodeTypes[first] === NodeType.Text &&
  9992. _nodeNextSiblings[first] === 0
  9993. ? first
  9994. : 0;
  9995. };
  9996. /**
  9997. * Whether two elements carry exactly the same attributes. Values compare raw:
  9998. * two spellings of one value are rare enough not to be worth decoding for, and
  9999. * reading them as different only declines a merge.
  10000. * @param {HtmlPath} path the accessor
  10001. * @param {HtmlElement} one an element
  10002. * @param {HtmlElement} other another
  10003. * @returns {boolean} true when the two agree on every attribute
  10004. */
  10005. const _sameAttributes = (path, one, other) => {
  10006. const count = path.attributeCount(one);
  10007. if (count !== path.attributeCount(other)) return false;
  10008. for (let i = 0; i < count; i++) {
  10009. const attribute = path.attributeAt(i, one);
  10010. const match = path.findAttribute(path.attributeName(attribute), other);
  10011. if (
  10012. match === 0 ||
  10013. path.attributeValue(match) !== path.attributeValue(attribute)
  10014. ) {
  10015. return false;
  10016. }
  10017. }
  10018. return true;
  10019. };
  10020. /**
  10021. * The `<style>` that may be folded into `element`: the next sibling, when only
  10022. * whitespace lies between them and the two agree on every attribute. Adjacency
  10023. * is what makes the fold safe — nothing moves past anything, so the cascade is
  10024. * the one the source wrote. Media, `title` and `blocking` all ride on the
  10025. * attribute test, and `type` on both being CSS in the first place.
  10026. * @param {HtmlPath} path the accessor
  10027. * @param {HtmlElement} element a `<style>` element
  10028. * @returns {HtmlElement} the next `<style>` of the run, or 0
  10029. */
  10030. const _mergeableStyleAfter = (path, element) => {
  10031. for (
  10032. let next = _nodeNextSiblings[element];
  10033. next !== 0;
  10034. next = _nodeNextSiblings[next]
  10035. ) {
  10036. const type = _nodeTypes[next];
  10037. if (type === NodeType.Text) {
  10038. if (!isAllWs(_nodeStrings[next])) return 0;
  10039. continue;
  10040. }
  10041. return type === NodeType.Element &&
  10042. path.namespace(next) === NS_HTML &&
  10043. path.tagName(next) === "style" &&
  10044. _styleTextChild(next) !== 0 &&
  10045. _isCssStyleElement(path, next) &&
  10046. _sameAttributes(path, element, next)
  10047. ? next
  10048. : 0;
  10049. }
  10050. return 0;
  10051. };
  10052. /**
  10053. * The text the first `<style>` of a run prints: every sheet in it, each minified
  10054. * on its own and then joined. Marks the rest of the run absorbed, which is what
  10055. * makes their tags and text print as nothing — it runs first because a run's
  10056. * first element prints before any of the others in both print paths.
  10057. *
  10058. * Each sheet is minified before it is joined, never after: only text the
  10059. * minifier accepted is known to be terminated, and appending to a sheet that is
  10060. * not makes the next one part of the last rule of this one. A sheet after the
  10061. * first carrying `@import` (or `@charset` / `@namespace`) is not folded either —
  10062. * those only apply at the top of a sheet, and would silently stop applying.
  10063. * @param {HtmlPath} path the accessor
  10064. * @param {HtmlElement} element the run's first `<style>`
  10065. * @param {string} css its own text
  10066. * @returns {string} the run's text
  10067. */
  10068. const _mergedStyleRun = (path, element, css) => {
  10069. const head = _minifyStyleBody(css);
  10070. if (head === null) return css;
  10071. let out = head;
  10072. for (
  10073. let next = _mergeableStyleAfter(path, element);
  10074. next !== 0;
  10075. next = _mergeableStyleAfter(path, next)
  10076. ) {
  10077. const decided = { text: "" };
  10078. const body = _minifyStyleBody(
  10079. _nodeStrings[/** @type {HtmlNodeRef} */ (_styleTextChild(next))],
  10080. decided
  10081. );
  10082. if (body === null) break;
  10083. // Read the built-in's text, not the marker: whether this sheet may be
  10084. // absorbed decides what its own element prints, so it cannot wait for an
  10085. // answer.
  10086. if (_SHEET_LEADING_AT_RULE_REGEXP.test(decided.text || body)) break;
  10087. if (_absorbedStyles === null) _absorbedStyles = new Set();
  10088. _absorbedStyles.add(next);
  10089. out += body;
  10090. }
  10091. return out;
  10092. };
  10093. /**
  10094. * Re-serialize a srcset with its whitespace reduced to what the grammar needs.
  10095. * The candidate list is unchanged — only the bytes between candidates go — and
  10096. * anything the parser rejects is handed back untouched.
  10097. * @param {string} raw raw (undecoded) attribute value
  10098. * @returns {string} the normalized value
  10099. */
  10100. const _normalizeSrcset = (raw) => {
  10101. let candidates;
  10102. try {
  10103. candidates = parseSrcset(raw);
  10104. } catch (_err) {
  10105. return raw;
  10106. }
  10107. if (candidates.length === 0) return raw;
  10108. let out = "";
  10109. let previousBare = false;
  10110. for (let i = 0; i < candidates.length; i++) {
  10111. const url = candidates[i][0];
  10112. const end = candidates[i][2];
  10113. const nextStart =
  10114. i + 1 < candidates.length ? candidates[i + 1][1] : raw.length;
  10115. let descriptor = raw.slice(end, nextStart);
  10116. const comma = descriptor.indexOf(",");
  10117. if (comma !== -1) descriptor = descriptor.slice(0, comma);
  10118. descriptor = _asciiTrim(descriptor).replace(/[\t\n\f\r ]+/g, " ");
  10119. // A descriptor-less URL runs to the next whitespace, so without one it
  10120. // would swallow the separating comma and the URL behind it.
  10121. if (i !== 0) out += previousBare ? ", " : ",";
  10122. out += descriptor === "" ? url : `${url} ${descriptor}`;
  10123. previousBare = descriptor === "";
  10124. }
  10125. return out;
  10126. };
  10127. /**
  10128. * ASCII-lowercase a name or a keyword value, which is all HTML folds — a Unicode
  10129. * `toLowerCase` maps characters the parser leaves alone (U+212A onto `k`).
  10130. * Returns the input untouched when there is nothing to fold (the common case).
  10131. * @param {string} value source name or value
  10132. * @returns {string} the value as the parser would match it
  10133. */
  10134. const _asciiLowerCase = (value) => {
  10135. for (let i = 0; i < value.length; i++) {
  10136. const c = value.charCodeAt(i);
  10137. if (c >= 0x41 && c <= 0x5a) {
  10138. return (
  10139. value.slice(0, i) +
  10140. value
  10141. .slice(i)
  10142. .replace(/[A-Z]/g, (u) => String.fromCharCode(u.charCodeAt(0) + 0x20))
  10143. );
  10144. }
  10145. }
  10146. return value;
  10147. };
  10148. /**
  10149. * Whether a raw attribute value keeps its meaning unquoted: §13.1.2.3 admits a
  10150. * non-empty run with no whitespace, quote, backtick, `=`, `<` or `>`. Character
  10151. * references decode identically either way, so `&` needs no guard.
  10152. * @param {string} raw raw (undecoded) value, without its quotes
  10153. * @returns {boolean} true when the unquoted form re-parses to the same value
  10154. */
  10155. const _canUnquoteAttributeValue = (raw) => {
  10156. for (let i = 0; i < raw.length; i++) {
  10157. const c = raw.charCodeAt(i);
  10158. if (
  10159. c === 0x20 ||
  10160. c === 0x22 ||
  10161. c === 0x27 ||
  10162. c === 0x3c ||
  10163. c === 0x3d ||
  10164. c === 0x3e ||
  10165. c === 0x60 ||
  10166. (c >= 0x09 && c <= 0x0d)
  10167. ) {
  10168. return false;
  10169. }
  10170. }
  10171. return raw.length !== 0;
  10172. };
  10173. // How often each attribute name occurs in the document being printed, or null
  10174. // until `sortAttributes` asks for it. Built from the parse's own attribute
  10175. // column rather than by walking the tree: every attribute of every element is
  10176. // already interned there, in one flat run.
  10177. /** @type {Map<string, number> | null} */
  10178. let _attributeFrequencies = null;
  10179. // Reused by `_sortedAttributes`: one element's attribute indices and their
  10180. // ranks, so ordering a tag allocates nothing.
  10181. /** @type {number[]} */
  10182. const _sortOrderScratch = [];
  10183. /** @type {number[]} */
  10184. const _sortKeyScratch = [];
  10185. // The print order is a property of the document, so resolving a name to its
  10186. // rank once leaves the per-element sort comparing two integers.
  10187. /** @type {Map<string, number> | null} */
  10188. let _attributeRanks = null;
  10189. /**
  10190. * @returns {Map<string, number>} each attribute name's place in the print order
  10191. */
  10192. const _attributeRank = () => {
  10193. if (_attributeRanks !== null) return _attributeRanks;
  10194. const frequency = _attributeFrequency();
  10195. const names = [...frequency.keys()].sort((a, b) => {
  10196. // Commonest names first, so the run two elements share is one run of bytes
  10197. // a compressor pays for once. Name order breaks ties, keeping this total.
  10198. const byFrequency =
  10199. /** @type {number} */ (frequency.get(b)) -
  10200. /** @type {number} */ (frequency.get(a));
  10201. if (byFrequency !== 0) return byFrequency;
  10202. return a < b ? -1 : 1;
  10203. });
  10204. const ranks = new Map();
  10205. for (let i = 0; i < names.length; i++) ranks.set(names[i], i);
  10206. _attributeRanks = ranks;
  10207. return ranks;
  10208. };
  10209. /**
  10210. * @returns {Map<string, number>} how often each attribute name occurs
  10211. */
  10212. const _attributeFrequency = () => {
  10213. if (_attributeFrequencies !== null) return _attributeFrequencies;
  10214. const frequency = new Map();
  10215. for (let i = 1; i <= _attrCount; i++) {
  10216. const name = _attrNames[i];
  10217. frequency.set(name, (frequency.get(name) || 0) + 1);
  10218. }
  10219. _attributeFrequencies = frequency;
  10220. return frequency;
  10221. };
  10222. /**
  10223. * The element's attribute indices in the order `sortAttributes` prints them:
  10224. * the document's commonest names first, ties by name. Nothing in HTML reads
  10225. * attribute order, so a stable spelling across pages is worth the repeats it
  10226. * hands a compressor.
  10227. * @param {HtmlPath} path the accessor positioned on the element
  10228. * @param {number} count its attribute count
  10229. * @returns {number[]} the indices, in print order
  10230. */
  10231. const _sortedAttributes = (path, count) => {
  10232. const ranks = _attributeRank();
  10233. const order = _sortOrderScratch;
  10234. const keys = _sortKeyScratch;
  10235. for (let i = 0; i < count; i++) {
  10236. order[i] = i;
  10237. // The ranks were counted off the same column this name is read from, so
  10238. // every name printed here has one.
  10239. keys[i] = /** @type {number} */ (
  10240. ranks.get(path.attributeName(path.attributeAt(i)))
  10241. );
  10242. }
  10243. // An element carries a handful of attributes, so a stable insertion sort over
  10244. // their ranks beats `Array#sort`'s setup and the comparator it would call.
  10245. for (let i = 1; i < count; i++) {
  10246. const key = keys[i];
  10247. const index = order[i];
  10248. let j = i - 1;
  10249. while (j >= 0 && keys[j] > key) {
  10250. keys[j + 1] = keys[j];
  10251. order[j + 1] = order[j];
  10252. j--;
  10253. }
  10254. keys[j + 1] = key;
  10255. order[j + 1] = index;
  10256. }
  10257. return order;
  10258. };
  10259. /**
  10260. * Rewrite an opening tag from its own source spans, collapsing the whitespace
  10261. * between its attributes to one space and dropping the quotes around any value
  10262. * that is legal unquoted (§13.1.2.3). webpack's own late asset passes match the
  10263. * sentinels they left behind with the quote optional, so they still see them
  10264. * (see `INTEGRITY_SENTINEL_REGEXP`); a third-party post-processor that regexes
  10265. * emitted HTML assuming quotes would not, which is why `removeAttributeQuotes`
  10266. * is off by default in html-minifier.
  10267. *
  10268. * Falls back to the source tag when an attribute lies outside it — a repeated
  10269. * `<html>` / `<body>` tag merges its attributes onto the element already open,
  10270. * and those cannot be sliced from here.
  10271. * @param {HtmlPath} path the accessor positioned on the element
  10272. * @param {string} open the opening tag's source text
  10273. * @returns {string} the minified opening tag
  10274. */
  10275. const _minifyOpenTag = (path, open) => {
  10276. const tagStart = path.start();
  10277. const tagEnd = path.tagEnd();
  10278. const count = path.attributeCount();
  10279. // HTML parsing ASCII-lowercases names, so folding one changes nothing. Foreign
  10280. // content keeps its source bytes: SVG admits element names no adjustment table
  10281. // would case-correct on the way back in.
  10282. const html = path.namespace() === NS_HTML;
  10283. const nameEnd = path.nameEnd() - tagStart;
  10284. // A bare `<name>` — half of all tags — is what the rest would rebuild byte
  10285. // for byte; decided before the name is sliced, so it allocates nothing.
  10286. if (count === 0 && open.length === nameEnd + 1) {
  10287. let folds = false;
  10288. if (html) {
  10289. for (let i = 1; i < nameEnd; i++) {
  10290. const c = open.charCodeAt(i);
  10291. if (c >= 0x41 && c <= 0x5a) {
  10292. folds = true;
  10293. break;
  10294. }
  10295. }
  10296. }
  10297. if (!folds) return open;
  10298. }
  10299. const sourceName = open.slice(1, nameEnd);
  10300. const elementName = html ? _asciiLowerCase(sourceName) : sourceName;
  10301. let out = `<${elementName}`;
  10302. const isViewportMeta =
  10303. html && path.tagName() === "meta" && _isViewportMeta(path);
  10304. let unquotedTail = false;
  10305. let consumedEnd = path.nameEnd();
  10306. // HTML only, which is also what keeps `consumedEnd` meaningful: it is read
  10307. // once more below, and only for a foreign element's `/>`.
  10308. // Per element, not per attribute: every attribute of one tag gets the same
  10309. // answer out of these, and they are the hottest lookups in the printer.
  10310. const markers =
  10311. html && _removeRedundantAttributes !== "none"
  10312. ? REDUNDANT_TYPE_ATTRIBUTES[elementName]
  10313. : undefined;
  10314. const defaults =
  10315. html && _removeRedundantAttributes === "all"
  10316. ? REDUNDANT_DEFAULT_ATTRIBUTES[elementName]
  10317. : undefined;
  10318. const mayDrop = markers !== undefined || defaults !== undefined;
  10319. const enumeratedOn = html ? ENUMERATED_KEYWORDS[elementName] : undefined;
  10320. const order =
  10321. _sortAttributes && html && count > 1
  10322. ? _sortedAttributes(path, count)
  10323. : null;
  10324. for (let k = 0; k < count; k++) {
  10325. const i = order === null ? k : order[k];
  10326. const attribute = path.attributeAt(i);
  10327. const nameStart = path.attributeNameStart(attribute);
  10328. const nameEnd = path.attributeNameEnd(attribute);
  10329. if (nameStart < tagStart || nameEnd > tagEnd) return open;
  10330. // The parse interned exactly `_asciiLowerCase(<name slice>)` — read it back
  10331. // rather than re-slicing and re-folding. Foreign content keeps source case.
  10332. const attributeName = html
  10333. ? path.attributeName(attribute)
  10334. : open.slice(nameStart - tagStart, nameEnd - tagStart);
  10335. const valueStart = path.attributeValueStart(attribute);
  10336. if (valueStart === -1) {
  10337. // Valueless is the empty value, so it goes with the empty ones.
  10338. if (html && _isRemovableEmptyAttribute(elementName, attributeName, "")) {
  10339. consumedEnd = nameEnd;
  10340. continue;
  10341. }
  10342. out += ` ${attributeName}`;
  10343. unquotedTail = false;
  10344. consumedEnd = nameEnd;
  10345. continue;
  10346. }
  10347. // `attributeValueStart` points past the opening quote, `attributeValueEnd`
  10348. // at the closing one, so a quoted value's span is one wider on each side.
  10349. const quote = open.charCodeAt(valueStart - 1 - tagStart);
  10350. const quoted = quote === 34 || quote === 39;
  10351. const rawEnd = path.attributeValueEnd(attribute);
  10352. const valueEnd = rawEnd + (quoted ? 1 : 0);
  10353. const rawValue = open.slice(valueStart - tagStart, rawEnd - tagStart);
  10354. // Dropped before any rewrite, and whether or not the source quoted it:
  10355. // the value only restates the default, or carries nothing at all.
  10356. if (
  10357. html &&
  10358. ((mayDrop &&
  10359. _isRedundantAttribute(
  10360. markers,
  10361. defaults,
  10362. elementName,
  10363. attributeName,
  10364. rawValue
  10365. )) ||
  10366. _isRemovableEmptyAttribute(elementName, attributeName, rawValue))
  10367. ) {
  10368. consumedEnd = valueEnd;
  10369. continue;
  10370. }
  10371. if (quoted) {
  10372. const hasReference = rawValue.includes("&");
  10373. // A whole document, so it is offered decoded and written back escaped —
  10374. // the one attribute whose reference-carrying value is still rewritten,
  10375. // since a document is mostly references. The renderer cannot parse it
  10376. // here (the walk is mid-parse), so it looks up what a walk-only pass
  10377. // prepared, exactly as an async minifier does.
  10378. if (
  10379. html &&
  10380. (_renderEmbeddedSource !== undefined ||
  10381. (_deferEmbeddedSource !== undefined && _deferSrcdoc)) &&
  10382. attributeName === "srcdoc" &&
  10383. elementName === "iframe" &&
  10384. rawValue !== ""
  10385. ) {
  10386. const document = decodeEntities(rawValue, true);
  10387. // The whole attribute is deferred, not the document inside it: what
  10388. // escaping it needs is decided by what it holds, which is the answer.
  10389. // The same test the gate above makes: with both renderers set, a
  10390. // caller asking for `srcdoc` on the normal path gets it there.
  10391. if (_deferEmbeddedSource !== undefined && _deferSrcdoc) {
  10392. // Declining leaves the attribute exactly as it was written, down to
  10393. // the delimiter: re-escaping it into double quotes would lengthen a
  10394. // value the source spelled with single ones.
  10395. const delimiter = open[valueStart - tagStart - 1];
  10396. const asWritten = ` ${attributeName}=${delimiter}${rawValue}${delimiter}`;
  10397. out += _deferEmbedded(document, HTML_TYPE, (rendered) =>
  10398. typeof rendered !== "string" || rendered === document
  10399. ? asWritten
  10400. : ` ${attributeName}="${escapeAttribute(rendered, 34, true)}"`
  10401. );
  10402. unquotedTail = false;
  10403. consumedEnd = valueEnd;
  10404. continue;
  10405. }
  10406. // The synchronous renderer: `_renderEmbedded` defers where a collector
  10407. // is set, and this attribute is the one kept off that path.
  10408. const rendered = _renderEmbeddedOrNull(document, HTML_TYPE);
  10409. if (rendered !== null && rendered !== document) {
  10410. out += ` ${attributeName}="${escapeAttribute(rendered, 34, true)}"`;
  10411. unquotedTail = false;
  10412. consumedEnd = valueEnd;
  10413. continue;
  10414. }
  10415. }
  10416. // A character reference would decode to something these grammars read
  10417. // differently, so only a reference-free value is rewritten.
  10418. let value = rawValue;
  10419. if (!hasReference) {
  10420. if (html) {
  10421. value = _rewriteAttributeValue(
  10422. elementName,
  10423. attributeName,
  10424. rawValue,
  10425. isViewportMeta,
  10426. enumeratedOn
  10427. );
  10428. } else if (attributeName === "style") {
  10429. // The one foreign-content attribute HTML's rules still fit: SVG and
  10430. // MathML `style` is a CSS declaration list, same as HTML's. Every
  10431. // other name is adjusted or means something else there.
  10432. value = _minifyStyleAttribute(rawValue);
  10433. }
  10434. }
  10435. // An empty value is the bare name: the tokenizer's after-attribute-name
  10436. // state reads `x` in `<p x>` and `<p x="">` as the same attribute with
  10437. // the same empty value, in foreign content too. A boolean attribute
  10438. // spelled with its own name (`disabled="disabled"`) is the same empty
  10439. // presence, and that one the spec does canonicalize.
  10440. // Both spellings are the same empty presence, and writing either as the
  10441. // bare name is the `=""` going — a quoting decision, so it answers to
  10442. // the switch that names the rest of them.
  10443. if (
  10444. _transforms.normalizeAttributeQuotes &&
  10445. (rawValue === "" ||
  10446. (html &&
  10447. _transforms.collapseBooleanAttributes &&
  10448. rawValue.length === attributeName.length &&
  10449. _isBooleanAttribute(elementName, attributeName) &&
  10450. _asciiLowerCase(rawValue) === attributeName))
  10451. ) {
  10452. out += ` ${attributeName}`;
  10453. unquotedTail = false;
  10454. consumedEnd = valueEnd;
  10455. continue;
  10456. }
  10457. // Guarded on the reference, so the decode stays off the hot path — and
  10458. // tried before unquoting, which keeps every reference it writes bare.
  10459. const decoded = hasReference ? decodeEntities(rawValue, true) : null;
  10460. // A reference spelling whitespace is deliberate: writing it out would
  10461. // let `removeEmptyAttributes` read the attribute as empty on a re-run.
  10462. const spellable =
  10463. decoded !== null &&
  10464. (_asciiTrim(decoded) !== "" || _asciiTrim(rawValue) === "")
  10465. ? decoded
  10466. : null;
  10467. const isStyle = attributeName === "style";
  10468. // A `style=""` carrying a character reference is a declaration list like
  10469. // any other once decoded — `&quot;` is a quote to CSS, not an entity — so
  10470. // it is minified from the decoded text and spelled back from there.
  10471. const declarations =
  10472. isStyle && spellable !== null ? _minifyStyleAttribute(spellable) : null;
  10473. /**
  10474. * @param {string} minified the declaration list to write
  10475. * @returns {{ text: string, unquotedTail: boolean } | null} what to emit
  10476. */
  10477. const writeStyle = (minified) =>
  10478. declarations !== null
  10479. ? _writeAttribute(attributeName, rawValue, rawValue, minified)
  10480. : _writeAttribute(attributeName, minified, rawValue, spellable);
  10481. const written = isStyle
  10482. ? writeStyle(declarations !== null ? declarations : value)
  10483. : _writeAttribute(attributeName, value, rawValue, spellable);
  10484. if (written !== null) {
  10485. // The list is deferred, not the shape around it: whether this value
  10486. // leaves the tag ending unquoted is read back when the tag closes and
  10487. // cannot wait for an answer, so one that would land in the other shape
  10488. // is declined and the built-in's text stands.
  10489. out =
  10490. isStyle && _deferEmbeddedSource !== undefined
  10491. ? out +
  10492. _deferEmbedded(
  10493. // Decoded when the source spelled it with references, since
  10494. // that is the list itself rather than one attribute's spelling.
  10495. spellable !== null ? spellable : rawValue,
  10496. CSS_TYPE,
  10497. (rendered) => {
  10498. if (typeof rendered !== "string") return written.text;
  10499. const answered = writeStyle(rendered);
  10500. return answered !== null &&
  10501. answered.unquotedTail === written.unquotedTail
  10502. ? answered.text
  10503. : written.text;
  10504. },
  10505. BLOCK_CONTENTS
  10506. )
  10507. : out + written.text;
  10508. unquotedTail = written.unquotedTail;
  10509. consumedEnd = valueEnd;
  10510. continue;
  10511. }
  10512. }
  10513. // A quoted value carries its quotes over; an unquoted one starts at the
  10514. // value itself.
  10515. out += ` ${attributeName}=${open.slice(
  10516. (quoted ? valueStart - 1 : valueStart) - tagStart,
  10517. valueEnd - tagStart
  10518. )}`;
  10519. unquotedTail = !quoted;
  10520. consumedEnd = valueEnd;
  10521. }
  10522. // A source `/>` self-closes a foreign element, so it survives there — but only
  10523. // when the `/` really is the flag: in `<a href=x/>` it is the last character of
  10524. // an unquoted value instead, and after one of those it needs a space or it
  10525. // would fuse into the value. An HTML element's parser ignores it, so it goes.
  10526. if (
  10527. path.namespace() !== NS_HTML &&
  10528. open.charCodeAt(open.length - 2) === 47 &&
  10529. tagEnd - 2 >= consumedEnd
  10530. ) {
  10531. return `${out}${unquotedTail ? " /" : "/"}>`;
  10532. }
  10533. return `${out}>`;
  10534. };
  10535. /**
  10536. * Whether a comment must survive minification: downlevel conditional comments
  10537. * (`<!--[if …]>` / `<![endif]-->`) drive IE branching, server-side includes
  10538. * (`<!--#…-->`) are directives, and a `<?…?>` bogus comment is a server-side
  10539. * template tag — all behavior-bearing. Every other comment is inert, so
  10540. * dropping it preserves meaning.
  10541. * @param {string} data comment data (between `<!--` and `-->`)
  10542. * @param {string} source the comment's source text, including its delimiters
  10543. * @returns {boolean} true to keep the comment
  10544. */
  10545. const _keepComment = (data, source) => {
  10546. // `<?php … ?>` / `<%… %>` reach the tree as bogus comments (§13.2.5.42) but
  10547. // are server-side directives, and the forms below drive IE branching or a
  10548. // server-side include — every one of them is code rather than a comment, so
  10549. // no level drops it.
  10550. if (source.charCodeAt(1) === 63) return true;
  10551. const trimmed = data.trimStart();
  10552. // `<![` covers the downlevel-revealed forms: `<!--<![endif]-->` closers and
  10553. // standalone `<![if …]>` openers.
  10554. if (
  10555. trimmed.startsWith("[if") ||
  10556. trimmed.startsWith("[endif") ||
  10557. trimmed.startsWith("<![") ||
  10558. trimmed.startsWith("#")
  10559. ) {
  10560. return true;
  10561. }
  10562. const kept = _commentsKept;
  10563. // Every other comment is inert, so `"some"` is where the two languages part:
  10564. // nothing left here carries anything a parser reads.
  10565. if (typeof kept === "boolean") return kept;
  10566. if (kept === "some") return false;
  10567. // A pattern or a predicate of the author's, over the comment's own text.
  10568. return kept(data) === true;
  10569. };
  10570. // The source open tag `_elementOpenTag` last read — its second result, passed
  10571. // out of band so the per-element pair costs no object.
  10572. let _elementOpenSource = "";
  10573. /**
  10574. * An element's open tag as it stands in the source, before any minifying; also
  10575. * left in `_elementOpenSource`.
  10576. * @param {HtmlPath} path positioned on the element
  10577. * @returns {string} its source open tag
  10578. */
  10579. const _openTagSource = (path) => {
  10580. // A repeated `<html>` / `<body>` start tag adds its unseen attributes to the
  10581. // open element and is itself dropped, so that element's source tag no longer
  10582. // spells its attributes — rebuild it rather than echo a stale one.
  10583. const source =
  10584. _mergedAttrNodes.size !== 0 && _mergedAttrNodes.has(path.node)
  10585. ? _synthesizeOpenTag(path)
  10586. : path.openTag();
  10587. _elementOpenSource = source;
  10588. return source;
  10589. };
  10590. /**
  10591. * An element's open tag as the printer would emit it; the source tag it was
  10592. * read from is left in `_elementOpenSource`.
  10593. * @param {HtmlPath} path positioned on the element
  10594. * @param {boolean} minify whether minifying
  10595. * @returns {string} the printed open tag
  10596. */
  10597. const _elementOpenTag = (path, minify) => {
  10598. const source = _openTagSource(path);
  10599. return minify && source !== "" ? _minifyOpenTag(path, source) : source;
  10600. };
  10601. /**
  10602. * An element's end tag as the printer would emit it.
  10603. * @param {HtmlPath} path positioned on the element
  10604. * @param {string} name its tag name
  10605. * @param {string} open its source open tag
  10606. * @param {boolean} innerEmpty whether it printed no children
  10607. * @param {boolean} minify whether minifying
  10608. * @returns {string} the end tag, `""` when it carries nothing
  10609. */
  10610. const _elementCloseTag = (path, name, open, innerEmpty, minify) => {
  10611. // Everything after `<plaintext>` is text, so an emitted end tag would re-parse
  10612. // as literal text; a source self-closing tag (`<rect/>` in foreign content)
  10613. // with no children needs none either.
  10614. if (
  10615. name === "plaintext" ||
  10616. (innerEmpty && open.endsWith("/>")) ||
  10617. (minify && _canOmitEndTag(path, name, path.node))
  10618. ) {
  10619. return "";
  10620. }
  10621. // `</name>` spelled by the source, falling back to the element's own name when
  10622. // there is no source end tag to echo. Read here rather than up front: an
  10623. // omitted end tag throws it away, and that is what minifying mostly does.
  10624. if (minify && path.namespace() === NS_HTML) {
  10625. // The source name folds to the element's own — the common case, answered
  10626. // off the raw range without building the source spelling to cut apart.
  10627. if (
  10628. rangeEqualsLowerCase(_htmlSource, path.start() + 1, path.nameEnd(), name)
  10629. ) {
  10630. return `</${name}>`;
  10631. }
  10632. return `</${_asciiLowerCase(path.closeTag().slice(2, -1)).trim() || name}>`;
  10633. }
  10634. const sourceClose = path.closeTag();
  10635. return sourceClose === "" ? `</${name}>` : sourceClose;
  10636. };
  10637. /**
  10638. * Whether re-parsing an implied `<html>` / `<body>`'s children without its start
  10639. * tag would not hand them back: a start tag `inBody` ignores still closed
  10640. * `<head>` on the way there, so a head element written after one belongs to the
  10641. * body, and §13.2.6.4.6 gives it back to the head the modes had already left. A
  10642. * comment goes further out still, to the element above.
  10643. * @param {HtmlNodeRef} node the implied element
  10644. * @returns {boolean} true when its start tag has to materialize
  10645. */
  10646. const _contentOpensOutside = (node) => {
  10647. const first = _nodeFirstChildren[node];
  10648. if (first === 0) return false;
  10649. const type = _nodeTypes[first];
  10650. if (type === NodeType.Comment) return true;
  10651. return (
  10652. type === NodeType.Element &&
  10653. _namespaceOf(first) === NS_HTML &&
  10654. HEAD_ELEMENTS.has(_tagNameOf(first))
  10655. );
  10656. };
  10657. // What `_streamTags` decided for the element just entered — passed out of band
  10658. // so each streamed element costs no result object.
  10659. let _streamOpen = "";
  10660. let _streamClose = "";
  10661. let _streamImplied = "";
  10662. /**
  10663. * An element printed in pieces prints as `open + children + close`, and these are
  10664. * those two — the printer's `Element` case, minus everything in it that can only
  10665. * be decided once the children's text is in. They are left in `_streamOpen` /
  10666. * `_streamClose` / `_streamImplied`; `false` for an element not of that
  10667. * shape: a leading newline to round-trip, a self-closing tag, a `<template>`'s
  10668. * content fragment, or a source `/>` whose end tag hangs on whether anything
  10669. * printed inside it. `_streamImplied` names an omitted `<html>` / `<body>` whose
  10670. * open tag still materializes if the first thing inside it turns out to be
  10671. * whitespace.
  10672. * @param {HtmlPath} path positioned on the element
  10673. * @param {boolean} minify whether minifying
  10674. * @returns {boolean} whether the element prints in pieces
  10675. */
  10676. const _streamTags = (path, minify) => {
  10677. _streamImplied = "";
  10678. // Folded into the `<style>` before it, which already printed its text, or
  10679. // carrying nothing at all.
  10680. if (
  10681. minify &&
  10682. ((_absorbedStyles !== null && _absorbedStyles.has(path.node)) ||
  10683. _isRemovableEmptyElement(path, path.node))
  10684. ) {
  10685. _streamOpen = "";
  10686. _streamClose = "";
  10687. return true;
  10688. }
  10689. if (path.selfClosing() || path.templateContent() !== 0) return false;
  10690. // A fostered run that moves prints inside the table it came out of, which needs
  10691. // both of them held rather than emitted as the walk reaches them. One printing
  10692. // from source holds nothing: its content is a slice, so it still streams.
  10693. if (
  10694. _fosterHolders !== null &&
  10695. _fosterHolders.has(path.node) &&
  10696. ((_nodeFlags[path.node] & FLAG_FOSTER_REGION) === 0 ||
  10697. !(/** @type {Set<HtmlNodeRef>} */ (_fosterVerbatim).has(path.node)))
  10698. ) {
  10699. return false;
  10700. }
  10701. const name = path.tagName();
  10702. if (LEADING_NEWLINE_ELEMENTS.has(name)) return false;
  10703. if (name === "form" && _formNeedsPointerReset(path)) return false;
  10704. // An SVG subtree is offered to the renderer whole, so it has to be composed
  10705. // rather than streamed out in pieces that never form one string.
  10706. if (
  10707. (_renderEmbeddedSource !== undefined ||
  10708. _deferEmbeddedSource !== undefined) &&
  10709. name === "svg" &&
  10710. _isSvgRoot(path)
  10711. ) {
  10712. return false;
  10713. }
  10714. // Read before minifying: bailing afterwards would throw the work away and
  10715. // leave `printer` to redo it, minifying every `style=""` on a `/>`-spelled
  10716. // tag twice — and running a renderer twice over it.
  10717. const source = _openTagSource(path);
  10718. if (source.endsWith("/>")) return false;
  10719. const tag = minify && source !== "" ? _minifyOpenTag(path, source) : source;
  10720. if (minify && _canOmitStartTag(path, name, path.node, source)) {
  10721. _streamOpen = "";
  10722. _streamClose = _canOmitEndTag(path, name, path.node) ? "" : `</${name}>`;
  10723. return true;
  10724. }
  10725. if (source === "") {
  10726. if (path.attributeCount() === 0 && TRANSPARENT_IMPLIED_ELEMENTS.has(name)) {
  10727. // The parser re-implies the start tag, but not the end tag the source
  10728. // carried: without it whatever follows moves inside this element.
  10729. _streamClose = (
  10730. SHELL_ELEMENTS.has(name)
  10731. ? _shellEndTagOmittable(path, name, path.node)
  10732. : _canOmitEndTag(path, name, path.node)
  10733. )
  10734. ? ""
  10735. : `</${name}>`;
  10736. // Leading whitespace is decided on the first piece emitted; what the modes
  10737. // hand back out is known here, from the node itself.
  10738. const shell = name === "body" || name === "html";
  10739. const opensOutside = shell && _contentOpensOutside(path.node);
  10740. _streamOpen =
  10741. opensOutside || _impliedTableGroupStartNeeded(path, name, path.node)
  10742. ? `<${name}>`
  10743. : "";
  10744. _streamImplied = shell && !opensOutside ? name : "";
  10745. } else {
  10746. _streamOpen = _synthesizeOpenTag(path);
  10747. _streamClose = `</${name}>`;
  10748. }
  10749. return true;
  10750. }
  10751. _streamOpen = tag;
  10752. _streamClose = _elementCloseTag(path, name, source, false, minify);
  10753. return true;
  10754. };
  10755. /**
  10756. * Whether a `<form>` needs a `</form>` in front of it: one written inside another
  10757. * reaches the tree only because an end tag cleared the form element pointer on
  10758. * the way — §13.2.6.4.7 ignores the start tag while the pointer is set, so
  10759. * without it the element is dropped on re-parsing. A `<template>` between the two
  10760. * suspends the pointer instead, and needs nothing.
  10761. * @param {HtmlPath} path the accessor positioned on the `<form>`
  10762. * @returns {boolean} true when the end tag has to be written first
  10763. */
  10764. const _formNeedsPointerReset = (path) => {
  10765. // §13.2.6.5 inserts a foreign `<form>` without consulting the pointer at all.
  10766. if (path.namespace() !== NS_HTML) return false;
  10767. let found = false;
  10768. for (
  10769. let ancestor = path.parentOf();
  10770. ancestor !== 0;
  10771. ancestor = path.parentOf(ancestor)
  10772. ) {
  10773. // A template's content is a fragment, not an element: reaching it means the
  10774. // walk left a `<template>`, which suspends the pointer.
  10775. if (path.type(ancestor) === NodeType.DocumentFragment) return false;
  10776. if (path.type(ancestor) !== NodeType.Element) return found;
  10777. if (path.namespace(ancestor) !== NS_HTML) continue;
  10778. const name = path.tagName(ancestor);
  10779. // A template anywhere above suspends the pointer, whichever side of the
  10780. // outer form it is on, so the start tag is never the one that is ignored.
  10781. if (name === "template") return false;
  10782. if (name === "form") found = true;
  10783. }
  10784. return found;
  10785. };
  10786. /**
  10787. * Whether printing a fostered run back inside its table hands the parser the same
  10788. * tree: only when "in table" fosters every node of it straight back out. What it
  10789. * keeps instead — its own content, and the start tags §13.2.6.4.9 gives a rule of
  10790. * their own — would stay in the table, and a formatting element §13.2.4.3 rebuilt
  10791. * has no source tag to place it with.
  10792. * @param {HtmlPath} path the accessor
  10793. * @param {HtmlNodeRef} node the run's root
  10794. * @param {0 | 1} needed 1 once a start tag in it is known to end something open
  10795. * @returns {0 | 1 | 2 | 3} 0 = stays put, 1 = replays, 2 = replays and is needed,
  10796. * 3 = has to move but cannot replay, so the table prints from its own source
  10797. */
  10798. const _fosterRunReplays = (path, node, needed) => {
  10799. let blocked = false;
  10800. for (let n = node; ;) {
  10801. if (path.type(n) === NodeType.Element) {
  10802. const html = path.namespace(n) === NS_HTML;
  10803. const name = html ? path.tagName(n) : "";
  10804. // Only the root has to be one "in table" fosters: everything under it was
  10805. // inserted at the same current node either way.
  10806. if (n === node && html && TABLE_CONTEXT.has(name)) blocked = true;
  10807. if (
  10808. html &&
  10809. ((FORMATTING.has(name) && path.openTag(n) === "") ||
  10810. (n === node &&
  10811. (name === "input"
  10812. ? _isHiddenInput(path, n)
  10813. : name === "form"
  10814. ? path.firstChild(n) !== 0
  10815. : FOSTER_KEPT.has(name))))
  10816. ) {
  10817. blocked = true;
  10818. }
  10819. if (!html) {
  10820. const child = path.firstChild(n);
  10821. if (child !== 0) {
  10822. n = child;
  10823. continue;
  10824. }
  10825. }
  10826. // A scope rule ends the nearest match on the stack, not the parent, so
  10827. // every element still open above it is a candidate.
  10828. let a = html ? path.parentOf(n) : 0;
  10829. while (needed === 0 && a !== 0) {
  10830. if (
  10831. path.type(a) === NodeType.Element &&
  10832. path.namespace(a) === NS_HTML &&
  10833. _startTagEnds(path.tagName(a), name)
  10834. ) {
  10835. needed = 1;
  10836. }
  10837. a = path.parentOf(a);
  10838. }
  10839. }
  10840. const child = path.firstChild(n);
  10841. if (child !== 0) {
  10842. n = child;
  10843. continue;
  10844. }
  10845. for (;;) {
  10846. if (n === node) {
  10847. return needed === 1 ? (blocked ? 3 : 2) : blocked ? 0 : 1;
  10848. }
  10849. const next = path.nextSibling(n);
  10850. if (next !== 0) {
  10851. n = next;
  10852. break;
  10853. }
  10854. n = path.parentOf(n);
  10855. }
  10856. }
  10857. };
  10858. /**
  10859. * Whether an `<input>` is the one §13.2.6.4.9 keeps rather than fosters: every
  10860. * other one takes the "anything else" arc out of the table.
  10861. * @param {HtmlPath} path the accessor
  10862. * @param {HtmlNodeRef} node the `<input>`
  10863. * @returns {boolean} true when the table would keep it
  10864. */
  10865. /**
  10866. * Whether a `</form>` written in front of the form about to be inserted would
  10867. * take something with it: §13.2.6.4.7 generates implied end tags before removing
  10868. * the form, and the source's own end tag ran with nothing of the sort open.
  10869. * @returns {boolean} true when one is open between here and the enclosing form
  10870. */
  10871. const _formResetWouldPop = () => {
  10872. // §13.2.6.4.7 removes the form from the stack rather than popping to it, so the
  10873. // enclosing one is in the tree and not on the stack — the walk is up the tree.
  10874. let popped = false;
  10875. for (
  10876. let n = open.length === 0 ? 0 : open[open.length - 1];
  10877. n !== 0;
  10878. n = _nodeParents[n]
  10879. ) {
  10880. if (_nodeTypes[n] !== NodeType.Element || _namespaceOf(n) !== NS_HTML) {
  10881. continue;
  10882. }
  10883. const name = _tagNameOf(n);
  10884. if (name === "form") return popped;
  10885. if (IMPLIED.has(name)) popped = true;
  10886. }
  10887. return false;
  10888. };
  10889. /**
  10890. * Whether an entry of this name sits below the marker a §13.2.4.3 scan stopped
  10891. * at. The marker is what hides it, and the marker is there because an element
  10892. * the source left open put it there — so the tree is a shape only that element
  10893. * staying open reproduces.
  10894. * @param {number} from index the scan stopped at
  10895. * @param {string} name the tag name it was looking for
  10896. * @returns {boolean} true when the marker is hiding one
  10897. */
  10898. const _afeNameBelowMarker = (from, name) => {
  10899. for (let i = from - 1; i >= 0; i--) {
  10900. const e = afe[i];
  10901. if (e === AFE_MARKER) continue;
  10902. if (_namespaceOf(e) === NS_HTML && _tagNameOf(e) === name) return true;
  10903. }
  10904. return false;
  10905. };
  10906. /**
  10907. * Whether an `<input>` is the one §13.2.6.4.9 keeps rather than fosters: every
  10908. * other one takes the "anything else" arc out of the table.
  10909. * @param {HtmlPath} path the accessor
  10910. * @param {HtmlNodeRef} node the `<input>`
  10911. * @returns {boolean} true when the table would keep it
  10912. */
  10913. const _isHiddenInput = (path, node) => {
  10914. for (let i = 0, n = path.attributeCount(node); i < n; i++) {
  10915. const a = path.attributeAt(i, node);
  10916. if (path.attributeName(a) === "type") {
  10917. return _asciiLowerCase(path.attributeValue(a)) === "hidden";
  10918. }
  10919. }
  10920. return false;
  10921. };
  10922. /**
  10923. * Whether a start tag ends an open element of `openName` through a scope check —
  10924. * §4.13's followers, a second tag of the same name, one heading behind another,
  10925. * and the set whose members end each other by generating implied end tags. Inside
  10926. * a table none of them reach it, every scope stopping at the table; printed back
  10927. * with no table between them they do.
  10928. * @param {string} openName the open element's lowercased tag name
  10929. * @param {string} name the start tag's lowercased name
  10930. * @returns {boolean} true when the start tag ends it
  10931. */
  10932. const _startTagEnds = (openName, name) => {
  10933. if (name === openName) return true;
  10934. const ended = SCOPE_ENDED_BY.get(openName);
  10935. if (ended !== undefined && ended !== null && ended.has(name)) return true;
  10936. const closers = OPTIONAL_END_TAG_FOLLOWERS.get(openName);
  10937. if (closers !== undefined && closers.has(name)) return true;
  10938. // §4.13's tag-omission list is not §13.2.6.4.7's close-a-p list — `center` is in
  10939. // one and not the other, and the parser's is what decides the tree.
  10940. if (openName === "p" && BLOCK_START.has(name)) return true;
  10941. if (HEADING.has(openName)) return HEADING.has(name);
  10942. return IMPLIED.has(openName) && IMPLIED.has(name);
  10943. };
  10944. /**
  10945. * Whether `table`'s fostered run prints back inside it, memoized so the holder
  10946. * that skips the run and the table that takes it in answer this the same way.
  10947. * Moving it is a repair, not a preference: tree order is left alone unless a
  10948. * start tag in the run would close the holder on the way back in, and the run
  10949. * replays out of the table it is moved into.
  10950. * @param {HtmlPath} path the accessor
  10951. * @param {HtmlNodeRef} table the table the run came out of
  10952. * @returns {boolean} true when the run moves back in
  10953. */
  10954. const _fosterRunMoves = (path, table) => {
  10955. const moves = /** @type {Map<HtmlNodeRef, boolean>} */ (_fosterMoves);
  10956. const cached = moves.get(table);
  10957. if (cached !== undefined) return cached;
  10958. moves.set(table, false);
  10959. const run = /** @type {Map<HtmlNodeRef, HtmlNodeRef[]>} */ (
  10960. _fosteredRuns
  10961. ).get(table);
  10962. if (run === undefined) return false;
  10963. if (path.parentOf(table) === 0) return false;
  10964. let closes = false;
  10965. let text = false;
  10966. let blocked = false;
  10967. for (let i = 0; i < run.length; i++) {
  10968. const verdict = _fosterRunReplays(path, run[i], 0);
  10969. if (verdict === 0 || verdict === 3) {
  10970. blocked = true;
  10971. break;
  10972. }
  10973. if (verdict === 2) closes = true;
  10974. if (path.type(run[i]) === NodeType.Text) text = true;
  10975. }
  10976. // Interleaving reads both in source order, which holds only while they do not
  10977. // overlap: one written inside a sibling's span has no place among them.
  10978. if (!blocked) {
  10979. for (let i = 0; i < run.length && !blocked; i++) {
  10980. const at = path.start(run[i]);
  10981. for (let c = path.firstChild(table); c !== 0; c = path.nextSibling(c)) {
  10982. if (at > path.start(c) && at < _fosterRegionEnd(path, c)) {
  10983. blocked = true;
  10984. break;
  10985. }
  10986. }
  10987. }
  10988. }
  10989. // A table keeps whitespace-only text and fosters the rest: moved back in beside
  10990. // each other the two fuse, and one non-whitespace character fosters all of it.
  10991. // That blocks the re-nesting like any other node that cannot replay.
  10992. if (text && !blocked) {
  10993. for (let c = path.firstChild(table); c !== 0; c = path.nextSibling(c)) {
  10994. if (path.type(c) === NodeType.Text) {
  10995. blocked = true;
  10996. break;
  10997. }
  10998. }
  10999. }
  11000. // The run moves or stays whole, so one node needing to move and another unable
  11001. // to replay is the verbatim case — worth a second look only once blocked.
  11002. if (blocked) {
  11003. closes = false;
  11004. for (let i = 0; i < run.length && !closes; i++) {
  11005. const verdict = _fosterRunReplays(path, run[i], 0);
  11006. if (verdict === 2 || verdict === 3) closes = true;
  11007. }
  11008. }
  11009. if (!closes) return false;
  11010. // Nothing re-nested reproduces this one, so it prints the source it was built
  11011. // from and the run prints nothing: it is already inside that.
  11012. if (blocked) {
  11013. if (_fosterVerbatim === null) _fosterVerbatim = new Set();
  11014. // The holder, not the table: an earlier run in it prints end tags that leave
  11015. // the list of active formatting elements elsewhere before this one is read.
  11016. const holder = path.parentOf(table);
  11017. _fosterVerbatim.add(holder);
  11018. // Anything still open around it: the slice runs to where the source stopped,
  11019. // so an end tag none of them was given cannot be invented after it.
  11020. if (_fosterOpenTail === null) _fosterOpenTail = new Set();
  11021. for (let a = holder; a !== 0; a = path.parentOf(a)) {
  11022. _fosterOpenTail.add(a);
  11023. _nodeFlags[a] |= FLAG_FOSTER_REGION;
  11024. }
  11025. const reach = _fosterRegionEnd(path, table);
  11026. if (reach > _fosterSpanEnd) _fosterSpanEnd = reach;
  11027. moves.set(table, true);
  11028. return true;
  11029. }
  11030. moves.set(table, closes);
  11031. return closes;
  11032. };
  11033. /**
  11034. * Decide every fostered run before the walk starts and forget the ones that stay
  11035. * put: a holder whose run moves cannot print in pieces, and that costs its whole
  11036. * subtree the streamed path — a price only the runs that actually move should
  11037. * pay. With none left the walk runs exactly as it does for a document that never
  11038. * fostered anything.
  11039. * @param {HtmlPath} path the accessor
  11040. * @returns {void}
  11041. */
  11042. const _settleFosteredRuns = (path) => {
  11043. const log = /** @type {HtmlNodeRef[]} */ (_fosteredLog);
  11044. // Nothing is built for a document whose runs all stay put, which is most of
  11045. // them: one pass says whether any node would be moved at all.
  11046. let moves = false;
  11047. for (let i = 1; i < log.length; i += 2) {
  11048. const verdict = _fosterRunReplays(path, log[i], 0);
  11049. if (verdict === 2 || verdict === 3) {
  11050. moves = true;
  11051. break;
  11052. }
  11053. }
  11054. if (!moves) return;
  11055. /** @type {Map<HtmlNodeRef, HtmlNodeRef[]>} */
  11056. const runs = new Map();
  11057. for (let i = 0; i < log.length; i += 2) {
  11058. const run = runs.get(log[i]);
  11059. if (run === undefined) runs.set(log[i], [log[i + 1]]);
  11060. else run.push(log[i + 1]);
  11061. }
  11062. _fosteredRuns = runs;
  11063. _fosterMoves = new Map();
  11064. /** @type {Map<HtmlNodeRef, HtmlNodeRef>} */
  11065. const nodes = new Map();
  11066. /** @type {Set<HtmlNodeRef>} */
  11067. const holders = new Set();
  11068. for (let i = 0; i < log.length; i += 2) {
  11069. if (!_fosterRunMoves(path, log[i])) continue;
  11070. // The adoption agency can move a node after it is fostered, so the holder
  11071. // that skips the run is read from the tree, not from where it went in.
  11072. holders.add(path.parentOf(log[i + 1]));
  11073. nodes.set(log[i + 1], log[i]);
  11074. }
  11075. for (const table of runs.keys()) {
  11076. if (!_fosterRunMoves(path, table)) runs.delete(table);
  11077. }
  11078. _fosteredNodes = nodes;
  11079. _fosterHolders = holders;
  11080. };
  11081. /**
  11082. * The tree as a string, for the one check the printer cannot make from the tree
  11083. * alone: whether what it printed parses back to what it printed from. Values are
  11084. * compared as the DOM holds them, so the same attribute written with a different
  11085. * quote or escape is the same attribute.
  11086. * @param {HtmlNodeRef} root the node whose children are read
  11087. * @returns {string} a rendering equal for equal trees
  11088. */
  11089. const _digest = (root) => {
  11090. /** @type {string[]} */
  11091. const out = [];
  11092. // A stack rather than recursion: a document nests as deeply as its source says
  11093. // it does, and the tree this reads is the one the printer just walked.
  11094. /** @type {number[]} */
  11095. const stack = [];
  11096. const first = A.firstChild(root);
  11097. if (first !== 0) stack.push(first, 0);
  11098. while (stack.length !== 0) {
  11099. const depth = /** @type {number} */ (stack.pop());
  11100. const node = /** @type {HtmlNodeRef} */ (stack.pop());
  11101. // The sibling goes on first, so this node's own subtree drains before it.
  11102. const next = A.nextSibling(node);
  11103. if (next !== 0) stack.push(next, depth);
  11104. const type = A.type(node);
  11105. if (type !== NodeType.Element) {
  11106. out.push(`${depth}\u0001${type}\u0001${A.data(node)}`);
  11107. continue;
  11108. }
  11109. out.push(
  11110. `${depth}\u0001e\u0001${A.namespace(node)}\u0001${A.tagName(node)}`
  11111. );
  11112. const attributes = [];
  11113. for (const a of A.attributes(node)) {
  11114. // The value as the DOM holds it: the same value can be written with a
  11115. // different quote and a different escape and still be the same attribute.
  11116. attributes.push(
  11117. `${a.serializedName || a.name}=${decodeEntities(a.value, true)}`
  11118. );
  11119. }
  11120. attributes.sort();
  11121. out.push(attributes.join("\u0002"));
  11122. const content = A.templateContent(node);
  11123. const inner = content !== 0 ? A.firstChild(content) : A.firstChild(node);
  11124. if (inner !== 0) stack.push(inner, depth + 1);
  11125. }
  11126. return out.join("\n");
  11127. };
  11128. /**
  11129. * Whether this element is one the settled runs concern — the parent a run was
  11130. * fostered into, or the table it goes back inside. Every other element composes
  11131. * its children the way it always did.
  11132. * @param {HtmlNodeRef} node the element
  11133. * @returns {boolean} true when it has to read the run
  11134. */
  11135. const _fosterTouches = (node) =>
  11136. (_fosterVerbatim !== null && _fosterVerbatim.has(node)) ||
  11137. /** @type {Set<HtmlNodeRef>} */ (_fosterHolders).has(node) ||
  11138. /** @type {Map<HtmlNodeRef, HtmlNodeRef[]>} */ (_fosteredRuns).has(node);
  11139. /**
  11140. * Whether this element still owes an end tag once its content printed from
  11141. * source: one the source never wrote is not the printer's to invent, and one it
  11142. * did write is already inside the slice when the span ran past it.
  11143. * @param {HtmlPath} path the accessor positioned on the element
  11144. * @returns {boolean} true when the end tag still has to be printed
  11145. */
  11146. const _endTagOutsideSpan = (path) =>
  11147. path.sourceClosed() && path.end(path.node) > _fosterSpanEnd;
  11148. /**
  11149. * The source an element's content was built from. Nothing re-nested reproduces a
  11150. * run that lands here, and the source did — anchored on the element itself, so no
  11151. * child of it can start earlier and none is left out.
  11152. * @param {HtmlPath} path the accessor
  11153. * @param {HtmlNodeRef} node the element printing from source
  11154. * @returns {string} its raw content
  11155. */
  11156. const _fosterSourceRegion = (path, node) => {
  11157. let end = path.tagEnd(node);
  11158. // A fragment, or an element the parser implied, has no opening tag to start
  11159. // from — its content begins wherever the first thing in it was written.
  11160. let from = -1;
  11161. for (let c = path.firstChild(node); c !== 0; c = path.nextSibling(c)) {
  11162. const reach = _fosterRegionEnd(path, c);
  11163. if (reach > end) end = reach;
  11164. // Offset 0 is a real position for the first node in the source; a node the
  11165. // parser made has no range at all, which is both offsets at zero.
  11166. const at = path.start(c);
  11167. if ((at !== 0 || path.end(c) !== 0) && (from === -1 || at < from)) {
  11168. from = at;
  11169. }
  11170. }
  11171. if (from === -1 || from > end) from = path.tagEnd(node);
  11172. return path.sourceSpanAt(from, end);
  11173. };
  11174. /**
  11175. * An element's children's text when this document foster parented something: a
  11176. * run that moves prints inside the table it was written in rather than where the
  11177. * tree holds it, interleaved with the table's own children in source order —
  11178. * which is the order they were written in, so the parser fosters them right back
  11179. * out to where they are now.
  11180. * @param {HtmlPath} path the accessor positioned on the element
  11181. * @param {PrintContext} writer the print context
  11182. * @returns {string} the children's text
  11183. */
  11184. const _composeAroundFoster = (path, writer) => {
  11185. const node = path.node;
  11186. const fostered = /** @type {Map<HtmlNodeRef, HtmlNodeRef>} */ (
  11187. _fosteredNodes
  11188. );
  11189. const run = _fosterRunMoves(path, node)
  11190. ? /** @type {Map<HtmlNodeRef, HtmlNodeRef[]>} */ (_fosteredRuns).get(node)
  11191. : undefined;
  11192. /** @type {HtmlNodeRef[]} */
  11193. const order = run === undefined ? [] : [...run];
  11194. const moved = order.length !== 0;
  11195. for (let c = path.firstChild(); c !== 0; c = path.nextSibling(c)) {
  11196. const from = fostered.get(c);
  11197. if (from !== undefined && _fosterRunMoves(path, from)) continue;
  11198. order.push(c);
  11199. }
  11200. if (moved) order.sort((a, b) => path.start(a) - path.start(b));
  11201. if (
  11202. (_nodeFlags[node] & FLAG_FOSTER_REGION) !== 0 &&
  11203. /** @type {Set<HtmlNodeRef>} */ (_fosterVerbatim).has(node)
  11204. ) {
  11205. return _fosterSourceRegion(path, node);
  11206. }
  11207. let inner = "";
  11208. for (let i = 0; i < order.length; i++) inner += writer.get(order[i]);
  11209. return inner;
  11210. };
  11211. /**
  11212. * The last source offset the table's own tokens reach. Its recorded end stops at
  11213. * the start tag when the source never closed it, and the fostered nodes were
  11214. * written inside it, so the whole subtree and the run answer together.
  11215. * @param {HtmlPath} path the accessor
  11216. * @param {HtmlNodeRef} table the table printing from source
  11217. * @returns {number} end offset
  11218. */
  11219. const _fosterRegionEnd = (path, table) => {
  11220. let end = path.end(table);
  11221. const run = /** @type {Map<HtmlNodeRef, HtmlNodeRef[]>} */ (
  11222. _fosteredRuns
  11223. ).get(table);
  11224. const roots = run === undefined ? [table] : [table, ...run];
  11225. if (path.type(table) !== NodeType.Element) return end;
  11226. for (const root of roots) {
  11227. for (let n = root; ;) {
  11228. if (path.end(n) > end) end = path.end(n);
  11229. const child = path.firstChild(n);
  11230. if (child !== 0) {
  11231. n = child;
  11232. continue;
  11233. }
  11234. for (;;) {
  11235. if (n === root) break;
  11236. const next = path.nextSibling(n);
  11237. if (next !== 0) {
  11238. n = next;
  11239. break;
  11240. }
  11241. n = path.parentOf(n);
  11242. }
  11243. if (n === root) break;
  11244. }
  11245. }
  11246. return end;
  11247. };
  11248. /**
  11249. * The default HTML node printer — passed to the `SourceProcessor` and fired per
  11250. * node once its children are printed (a developer could supply their own). It
  11251. * takes the same `path` a visitor gets plus the print context as its `writer`,
  11252. * and knows nothing of the walk: it switches on `path.type()`, serializes the node
  11253. * — opening tags kept verbatim from source (attribute quoting / spacing
  11254. * preserved), end tags generated, text re-escaped from its decoded value — pulling
  11255. * its children's already-printed text from `writer.get`, and **returns** it. This
  11256. * is the WHATWG serialization, so the output re-parses to the same DOM (text-node
  11257. * offsets can overrun end tags, so source slices can't be used for text). The
  11258. * minify transforms are dropping inert comments and collapsing the whitespace
  11259. * between an opening tag's attributes; DOM-absent whitespace (between the
  11260. * doctype and `<html>`, etc.) naturally falls away.
  11261. * @param {HtmlPath} path the accessor positioned on the finished node
  11262. * @param {PrintContext} writer the print context (children's printed text)
  11263. * @returns {string} the node's serialized text
  11264. * @experimental exposed as `webpack.html.syntax.printer`; unstable API
  11265. */
  11266. const printer = (path, writer) => {
  11267. const minify = writer.options.mode === "minify";
  11268. switch (path.type()) {
  11269. case NodeType.Element: {
  11270. // Folded into the `<style>` before it, which printed the whole run, or
  11271. // carrying nothing at all.
  11272. if (
  11273. minify &&
  11274. ((_absorbedStyles !== null && _absorbedStyles.has(path.node)) ||
  11275. _isRemovableEmptyElement(path, path.node))
  11276. ) {
  11277. return "";
  11278. }
  11279. // Minifying rewrites the tag (attribute spacing / quoting) but never its
  11280. // `/>`-ness, which the end-tag decision below reads off the source.
  11281. const tag = _elementOpenTag(path, minify);
  11282. const open = _elementOpenSource;
  11283. // The whole subtree goes to the renderer, its nested bodies already
  11284. // rendered — children print before their parent.
  11285. const svgRoot =
  11286. minify &&
  11287. (_renderEmbeddedSource !== undefined ||
  11288. _deferEmbeddedSource !== undefined) &&
  11289. _isSvgRoot(path);
  11290. // Void / self-closing elements have no children and no end tag; a
  11291. // tag-less one (`<image>` → img, `</br>` → br) is rebuilt, not dropped.
  11292. if (path.selfClosing()) {
  11293. const selfClosed = open !== "" ? tag : _synthesizeOpenTag(path);
  11294. return svgRoot ? _renderEmbedded(selfClosed, SVG_TYPE) : selfClosed;
  11295. }
  11296. // `<template>` holds its children in a content fragment, not the child
  11297. // chain; every other element reconstructs from its children in order.
  11298. const tc = path.templateContent();
  11299. let inner = "";
  11300. if (tc !== 0) {
  11301. inner = writer.get(tc);
  11302. } else {
  11303. if (_fosteredRuns !== null && _fosterTouches(path.node)) {
  11304. inner = _composeAroundFoster(path, writer);
  11305. } else {
  11306. for (let c = path.firstChild(); c !== 0; c = path.nextSibling(c)) {
  11307. inner += writer.get(c);
  11308. }
  11309. }
  11310. // Round-trip the parser's leading-newline strip (`<pre>` / `<textarea>`
  11311. // / `<listing>`) so a value that starts with one survives re-parsing.
  11312. if (
  11313. inner.charCodeAt(0) === 10 &&
  11314. path.namespace() === NS_HTML &&
  11315. LEADING_NEWLINE_ELEMENTS.has(path.tagName())
  11316. ) {
  11317. inner = `\n${inner}`;
  11318. }
  11319. }
  11320. const name = path.tagName();
  11321. // The end tag is handled by the optional-end-tag rules like any other.
  11322. if (minify && _canOmitStartTag(path, name, path.node, open)) {
  11323. const implied = _canOmitEndTag(path, name, path.node)
  11324. ? ""
  11325. : `</${name}>`;
  11326. return inner + implied;
  11327. }
  11328. if (open === "") {
  11329. // A parser-implied structural element stays transparent — the parser
  11330. // re-implies it — but only when attribute-less (repeated `<html>` /
  11331. // `<body>` tags merge attributes onto the implied element).
  11332. if (
  11333. path.attributeCount() === 0 &&
  11334. TRANSPARENT_IMPLIED_ELEMENTS.has(name)
  11335. ) {
  11336. // Leading whitespace only survives re-parsing once `<body>` has
  11337. // started: before that the insertion modes drop it. Materializing
  11338. // the tag is the cheapest way to keep the text node intact.
  11339. if (
  11340. (name === "body" || name === "html") &&
  11341. (startsWithWs(inner) || _contentOpensOutside(path.node))
  11342. ) {
  11343. return `<${name}>${inner}`;
  11344. }
  11345. // §4.13 lets the start tag go only when the group before it kept
  11346. // its end tag, or that one would take these columns over.
  11347. if (_impliedTableGroupStartNeeded(path, name, path.node)) {
  11348. return `<${name}>${inner}`;
  11349. }
  11350. // The parser re-implies the start tag, but not the end tag the
  11351. // source carried: without it whatever follows moves inside.
  11352. return (
  11353. SHELL_ELEMENTS.has(name)
  11354. ? _shellEndTagOmittable(path, name, path.node)
  11355. : _canOmitEndTag(path, name, path.node)
  11356. )
  11357. ? inner
  11358. : `${inner}</${name}>`;
  11359. }
  11360. // Anything else tag-less (adoption-agency clone, reconstructed
  11361. // formatting element) must materialize or its formatting is lost.
  11362. return `${_synthesizeOpenTag(path) + inner}</${name}>`;
  11363. }
  11364. const composed =
  11365. (name === "form" && _formNeedsPointerReset(path) ? "</form>" : "") +
  11366. tag +
  11367. inner +
  11368. // Its content came out of the source unchanged, so an end tag the
  11369. // source never wrote would leave the parser somewhere else.
  11370. ((_nodeFlags[path.node] & FLAG_FOSTER_REGION) !== 0 &&
  11371. !_endTagOutsideSpan(path)
  11372. ? ""
  11373. : _elementCloseTag(path, name, open, inner === "", minify));
  11374. return svgRoot ? _renderEmbedded(composed, SVG_TYPE) : composed;
  11375. }
  11376. case NodeType.Text: {
  11377. const data = path.data();
  11378. // Literal-text elements (`script` / `style` / …) keep their body raw;
  11379. // every other text node is escaped (the WHATWG text split).
  11380. const parent = path.parentOf();
  11381. // TODO minify an inline `<script>` too: terser reaches webpack only
  11382. // through `minimizer-webpack-plugin` and its API is async, so it needs
  11383. // a JS-minify hook on the options rather than a call from here.
  11384. if (parent !== 0 && LITERAL_TEXT_PARENTS.has(path.tagName(parent))) {
  11385. if (!minify) return data;
  11386. const literalName = path.tagName(parent);
  11387. if (literalName === "style") {
  11388. if (!_isCssStyleElement(path, parent)) return data;
  11389. if (!_mergeStyles) {
  11390. const minified = _minifyStyleBody(data);
  11391. return minified === null ? data : minified;
  11392. }
  11393. // The run's first sheet prints the whole run; the rest printed
  11394. // into it here, before their own text or tags are reached.
  11395. return _absorbedStyles !== null && _absorbedStyles.has(parent)
  11396. ? ""
  11397. : _mergedStyleRun(path, parent, data);
  11398. }
  11399. if (literalName === "script") {
  11400. const type = _scriptType(path, parent);
  11401. if (JSON_SCRIPT_TYPES.has(type) || _JSON_SUBTYPE_REGEXP.test(type)) {
  11402. return _minifyInlineJson(data);
  11403. }
  11404. // No built-in: webpack ships no JS minifier, so an inline script is
  11405. // touched only by a caller's renderer.
  11406. if (
  11407. (_renderEmbeddedSource !== undefined ||
  11408. _deferEmbeddedSource !== undefined) &&
  11409. JAVASCRIPT_SCRIPT_TYPES.has(type) &&
  11410. data.trim() !== ""
  11411. ) {
  11412. return _renderEmbedded(data, JAVASCRIPT_TYPE);
  11413. }
  11414. }
  11415. return data;
  11416. }
  11417. // Whitespace directly under `<head>` or `<html>` is outside any block
  11418. // formatting context, so nothing ever renders it — the same inert-node
  11419. // reasoning that drops comments. `<body>`'s whitespace does render.
  11420. if (
  11421. minify &&
  11422. parent !== 0 &&
  11423. path.namespace(parent) === NS_HTML &&
  11424. (path.tagName(parent) === "head" || path.tagName(parent) === "html") &&
  11425. isAllWs(data)
  11426. ) {
  11427. return "";
  11428. }
  11429. // A `<` puts the node on the source-passthrough path below, which is what
  11430. // keeps `<%= x %>` off the escaper — worth more than the whitespace.
  11431. if (minify && _collapseWhitespace !== "none" && !data.includes("<")) {
  11432. // Only elements rendering whitespace verbatim without any CSS are
  11433. // excluded — by ancestor, since `white-space` inherits.
  11434. let preformatted = false;
  11435. for (
  11436. let ancestor = parent;
  11437. ancestor !== 0;
  11438. ancestor = path.parentOf(ancestor)
  11439. ) {
  11440. if (
  11441. path.namespace(ancestor) === NS_HTML &&
  11442. LEADING_NEWLINE_ELEMENTS.has(path.tagName(ancestor))
  11443. ) {
  11444. preformatted = true;
  11445. break;
  11446. }
  11447. }
  11448. if (!preformatted) {
  11449. let collapsed = collapseWhitespaceRuns(data);
  11450. // Past `"conservative"`, whitespace against a boundary no line box
  11451. // reaches is dropped rather than kept as one space.
  11452. if (_collapseWhitespace !== "conservative") {
  11453. const all = _collapseWhitespace === "all";
  11454. if (all || _startsOutsideLineBox(path, path.node, parent)) {
  11455. collapsed = collapsed.replace(/^ /, "");
  11456. }
  11457. if (all || _endsOutsideLineBox(path, path.node, parent)) {
  11458. collapsed = collapsed.replace(/ $/, "");
  11459. }
  11460. }
  11461. if (collapsed !== data) return _escapeTextContent(collapsed, minify);
  11462. }
  11463. }
  11464. const raw = path.source();
  11465. // Off-spec workaround: §13.3 escapes `<`/`>` in text, which would rewrite
  11466. // `<%= x %>` to `&lt;%= x %&gt;` in HTML webpack only passes through. Text
  11467. // that decoded to itself re-tokenizes to itself, so emit its source bytes.
  11468. // The guards are what keep that equivalence: every character reference is
  11469. // at least two characters longer than what it decodes to, so equal lengths
  11470. // mean nothing decoded — an unequal one means the node was merged or
  11471. // foster-parented and its range no longer describes `data`. The tail is
  11472. // checked separately because it is the only place concatenation can change
  11473. // tokenization: a `<`, or a `&` still open on a reference, would fuse with
  11474. // the next sibling once a comment between them is dropped. A CR is the one
  11475. // rewrite the length test cannot see — preprocessing maps a lone one to LF
  11476. // without changing length — so it is excluded by name.
  11477. if (
  11478. raw.length === data.length &&
  11479. raw.charCodeAt(raw.length - 1) !== 60 &&
  11480. !(inputHasCr && raw.includes("\r")) &&
  11481. !_hasOpenReference(raw)
  11482. ) {
  11483. return raw;
  11484. }
  11485. return _escapeTextContent(data, minify);
  11486. }
  11487. case NodeType.Doctype: {
  11488. // A leaf with no children, so its source is already tight. Parsing
  11489. // ASCII-lowercases the keyword and the name; the identifiers behind them
  11490. // are case-sensitive strings and stay as written.
  11491. const source = path.source();
  11492. if (!minify) return source;
  11493. const head = /^<!doctype[\t\n\f\r ]+([^\t\n\f\r >]+)/i.exec(source);
  11494. return head === null
  11495. ? source
  11496. : `<!doctype ${_asciiLowerCase(head[1])}${source.slice(
  11497. head[0].length
  11498. )}`;
  11499. }
  11500. case NodeType.Comment:
  11501. return minify && !_keepComment(path.data(), path.source())
  11502. ? ""
  11503. : path.source();
  11504. case NodeType.ProcessingInstruction:
  11505. // A leaf whose source is already tight, and unlike a comment it carries
  11506. // instructions for a consumer, so minification never drops it.
  11507. return path.source();
  11508. default: {
  11509. // Document / DocumentFragment: concatenate children in order.
  11510. if (_fosteredRuns !== null && _fosterTouches(path.node)) {
  11511. return _composeAroundFoster(path, writer);
  11512. }
  11513. let out = "";
  11514. for (let c = path.firstChild(); c !== 0; c = path.nextSibling(c)) {
  11515. out += writer.get(c);
  11516. }
  11517. return out;
  11518. }
  11519. }
  11520. };
  11521. // === Walk ===
  11522. // Walk state, hoisted to module scope so the walk helpers keep one function
  11523. // identity across parses and the streaming path can drive them mid-parse.
  11524. /** @type {CompiledVisitorMap} the between-parses value, so no map is retained */
  11525. const EMPTY_VISITORS = [];
  11526. /** @type {CompiledVisitorMap} */
  11527. let _visitors = EMPTY_VISITORS;
  11528. /** @type {PrintContext | undefined} */
  11529. let _writer;
  11530. // Iterative depth-first walk over the link columns: `firstChild` /
  11531. // `nextSibling` descend, the `parent` column ascends, so arbitrarily deep
  11532. // markup can't overflow the call stack (the old recursive walk died at
  11533. // ~10⁵ nesting). A `<template>`'s content fragment is visited before the
  11534. // element's children; ascending out of it continues with those children
  11535. // (the fragment is never in a sibling chain, `_nodeNextSiblings` = 0).
  11536. /**
  11537. * Fire a node's `enter` visitors; true when the walk may descend.
  11538. * @param {HtmlNodeRef} node node
  11539. * @returns {boolean} false when a visitor called `skipChildren()`
  11540. */
  11541. const _enterNode = (node) => {
  11542. // A holder printing from source composes out of the tree, not out of its
  11543. // children's text, so printing them would only be thrown away.
  11544. if (
  11545. (_nodeFlags[node] & FLAG_FOSTER_REGION) !== 0 &&
  11546. /** @type {Set<HtmlNodeRef>} */ (_fosterVerbatim).has(node)
  11547. ) {
  11548. return false;
  11549. }
  11550. const b = _visitors[_nodeTypes[node]];
  11551. if (b === undefined || b.enter.length === 0) return true;
  11552. _walkSkip = false;
  11553. _currentNode = node;
  11554. const p = _nodeParents[node];
  11555. _currentParent = p === 0 ? null : p;
  11556. const e = b.enter;
  11557. for (let i = 0; i < e.length; i++) e[i](A);
  11558. const skip = _walkSkip;
  11559. _walkSkip = false;
  11560. return !skip;
  11561. };
  11562. // === Printing in pieces ===
  11563. // The printer composes a node from its children's text, so the store holds every
  11564. // node's until the root is taken — several times the output on a document of any
  11565. // size. A node whose text is exactly `open + children + close` instead emits its
  11566. // opener as it opens and its closer as it exits, and each child goes straight out
  11567. // as it finishes; the store then only ever holds the subtrees that cannot (see
  11568. // `_streamTags`). Indexed by print depth, which is the open path.
  11569. /** @type {number} depth of the node the printing walk is on (maintained by `_walkRun`) */
  11570. let _printDepth = 0;
  11571. /** @type {number[]} 1 while the node at that depth prints in pieces */
  11572. const _printOpen = [];
  11573. /** @type {string[]} the closer the node at that depth still owes */
  11574. const _printClose = [];
  11575. // An omitted `<html>` / `<body>` only materializes when the first thing inside it
  11576. // is whitespace, which is known once the first piece is emitted — so its opener
  11577. // waits in the writer's pending stack until then.
  11578. /** @type {string[]} names of the elements whose openers are held back, innermost last */
  11579. const _printPendingNames = [];
  11580. /** @type {number[]} pending-stack slot of the node at that depth, -1 for none */
  11581. const _printPendingSlot = [];
  11582. /**
  11583. * Emit one piece of a node printed in pieces. The omitted openers held back above
  11584. * it decide first: an omitted `<html>` / `<body>` materializes exactly when the
  11585. * first thing inside it is whitespace, which the parser would otherwise drop on
  11586. * the way back in. Only the innermost can — once it does, everything outside it
  11587. * starts with a `<`.
  11588. * @param {string} text output text
  11589. */
  11590. const _emitPiece = (text) => {
  11591. if (text === "") return;
  11592. const w = /** @type {PrintContext} */ (_writer);
  11593. const held = _printPendingNames.length;
  11594. if (held !== 0) {
  11595. const c = text.charCodeAt(0);
  11596. if (c === 0x09 || c === 0x0a || c === 0x0c || c === 0x0d || c === 0x20) {
  11597. w.setPending(held - 1, `<${_printPendingNames[held - 1]}>`);
  11598. }
  11599. _printPendingNames.length = 0;
  11600. w.flushPending();
  11601. }
  11602. w.emitStreamed(text);
  11603. };
  11604. /**
  11605. * Decide whether the node just entered prints in pieces, and emit its opener if
  11606. * so. A node can only do that inside something that already is, so the decision
  11607. * walks down from the root and stops at the first that cannot.
  11608. * @param {HtmlNodeRef} node node
  11609. * @param {number} depth its depth in the walk
  11610. * @param {boolean} descending whether the walk may enter its children
  11611. */
  11612. const _openStreamed = (node, depth, descending) => {
  11613. const w = /** @type {PrintContext} */ (_writer);
  11614. let streams = 0;
  11615. _printPendingSlot[depth] = -1;
  11616. if (descending && (depth === 0 || _printOpen[depth - 1] === 1)) {
  11617. const ty = _nodeTypes[node];
  11618. if (ty === NodeType.Document || ty === NodeType.DocumentFragment) {
  11619. // Nothing of its own to print: its text is its children's, in order.
  11620. streams = 1;
  11621. _printClose[depth] = "";
  11622. } else if (ty === NodeType.Element) {
  11623. _currentNode = node;
  11624. if (_streamTags(A, w.options.mode === "minify")) {
  11625. streams = 1;
  11626. _printClose[depth] =
  11627. (_nodeFlags[node] & FLAG_FOSTER_REGION) !== 0 &&
  11628. !_endTagOutsideSpan(A)
  11629. ? ""
  11630. : _streamClose;
  11631. if (_streamImplied === "") {
  11632. _emitPiece(_streamOpen);
  11633. } else {
  11634. _printPendingSlot[depth] = w.pushPending("");
  11635. _printPendingNames.push(_streamImplied);
  11636. }
  11637. }
  11638. }
  11639. } else if (
  11640. (_nodeFlags[node] & FLAG_FOSTER_REGION) !== 0 &&
  11641. (depth === 0 || _printOpen[depth - 1] === 1) &&
  11642. /** @type {Set<HtmlNodeRef>} */ (_fosterVerbatim).has(node)
  11643. ) {
  11644. // Its children are skipped and its text is `open + that source + close`, so
  11645. // it streams like any other element rather than composing nothing.
  11646. _currentNode = node;
  11647. if (_streamTags(A, w.options.mode === "minify")) {
  11648. streams = 1;
  11649. _printClose[depth] = _endTagOutsideSpan(A) ? _streamClose : "";
  11650. const region = _fosterSourceRegion(A, node);
  11651. if (_streamImplied === "") {
  11652. _emitPiece(_streamOpen + region);
  11653. } else {
  11654. _printPendingSlot[depth] = w.pushPending("");
  11655. _printPendingNames.push(_streamImplied);
  11656. // The tag is still undecided; the content behind it is not, and nothing
  11657. // else will emit it because the children are skipped.
  11658. _emitPiece(region);
  11659. }
  11660. }
  11661. }
  11662. _printOpen[depth] = streams;
  11663. };
  11664. /**
  11665. * Fire a node's `exit` visitors, then — when printing — its printer (the
  11666. * post-order point: its children are already printed). `_writer` undefined =
  11667. * walk only.
  11668. * @param {HtmlNodeRef} node node
  11669. */
  11670. const _exitNode = (node) => {
  11671. const b = _visitors[_nodeTypes[node]];
  11672. if (b === undefined && _writer === undefined) return;
  11673. _currentNode = node;
  11674. const p = _nodeParents[node];
  11675. _currentParent = p === 0 ? null : p;
  11676. if (b !== undefined) {
  11677. const x = b.exit;
  11678. for (let i = 0; i < x.length; i++) x[i](A);
  11679. }
  11680. if (_writer === undefined) return;
  11681. const depth = _printDepth;
  11682. if (_printOpen[depth] === 1) {
  11683. // Printed in pieces: its opener went out as it opened and its children as
  11684. // they finished, so only its closer is left.
  11685. _emitPiece(_printClose[depth]);
  11686. const slot = _printPendingSlot[depth];
  11687. // Nothing inside it printed, so its held-back opener never had to.
  11688. if (slot !== -1 && _writer.isPending(slot)) {
  11689. _printPendingNames.pop();
  11690. _writer.dropPending();
  11691. }
  11692. return;
  11693. }
  11694. // Composed at once, but whatever holds it is being printed in pieces, so it
  11695. // goes straight out rather than into the store for a read-back that will never
  11696. // come.
  11697. if (depth !== 0 && _printOpen[depth - 1] === 1) {
  11698. _emitPiece(_writer.printPiece(A));
  11699. return;
  11700. }
  11701. _writer.printNode(node, A);
  11702. };
  11703. /**
  11704. * First node to visit inside `node` (template content before children).
  11705. * @param {HtmlNodeRef} node node
  11706. * @returns {HtmlNodeRef} first inner node (0 = leaf)
  11707. */
  11708. const _firstInner = (node) => {
  11709. const ty = _nodeTypes[node];
  11710. if (ty === NodeType.Element) {
  11711. const tc = _templateContentOf(node);
  11712. return tc !== 0 ? tc : _nodeFirstChildren[node];
  11713. }
  11714. if (ty === NodeType.Document || ty === NodeType.DocumentFragment) {
  11715. return _nodeFirstChildren[node];
  11716. }
  11717. return 0;
  11718. };
  11719. // === Streaming walk ===
  11720. // The walk follows the parser's open element stack: everything under the
  11721. // deepest *entered* element is finished markup, so it is visited and its node
  11722. // ids recycled, and peak storage is the open subtree rather than the whole
  11723. // document — the HTML analog of the CSS grammar's per-top-level-rule recycling.
  11724. //
  11725. // Streaming starts only once the document passes `_STREAM_MIN_NODES`: below it
  11726. // the walk is a single pass at EOF, which is what a small page wants anyway.
  11727. //
  11728. // Two rules keep it correct and cheap:
  11729. // - An element is entered only when something beneath it has to be flushed
  11730. // (`_FLUSH_BATCH` worth of nodes). Anything that opens and closes in between
  11731. // is never entered and is walked as one completed subtree, so its visitors
  11732. // see a final `end` and live child links.
  11733. // - Nothing is visited unless `_canFlush()` holds. An unsafe stretch (the
  11734. // adoption agency, tables, templates) just buffers, and the next sync sees
  11735. // the final tree.
  11736. //
  11737. // Only walk-only passes stream: printing holds every node's text until its
  11738. // parent prints.
  11739. /** @type {boolean} whether a parse is in progress, so re-entry can be refused */
  11740. let _parsing = false;
  11741. /** @type {boolean} whether this parse may stream its walk (owned by `grammar`) */
  11742. let _streamEligible = false;
  11743. /** set when the root's `enter` called `skipChildren()`, so only its `exit` fires */
  11744. let _docSkipped = false;
  11745. /** @type {boolean} whether the walk has started streaming (see `_STREAM_MIN_NODES`) */
  11746. let _streaming = false;
  11747. /** @type {HtmlElement[]} open elements whose `enter` has already fired */
  11748. const _entered = [];
  11749. /** `_entered` index whose `enter` called `skipChildren()` (-1 = none) */
  11750. let _skipFrom = -1;
  11751. /** set when `open` was spliced mid-stack, so the next sync scans it in full */
  11752. let _openSpliced = false;
  11753. // Set when a mid-stack `open.splice` removes an element the walk already
  11754. // entered (`</form>` with content still open inside it). The element leaves the
  11755. // open stack without its subtree ending, so the open stack no longer describes
  11756. // what has finished and reconciling against it would close and re-enter live
  11757. // elements. Rare enough to simply stop streaming for the rest of the parse: what
  11758. // is already visited stays visited, and the remainder is walked at EOF.
  11759. let _streamHalted = false;
  11760. /** set by `popOpen`: the open stack changed, so the walk has work at token end */
  11761. let _openStackChanged = false;
  11762. // Nodes that must pile up under the deepest entered element before a flush
  11763. // earns its walk setup. Closing an entered element always flushes, so this only
  11764. // bounds how much an un-entered subtree holds — a few KB of columns.
  11765. const _FLUSH_BATCH = 4096;
  11766. // Documents below this many nodes are walked once at EOF instead: recycling
  11767. // only pays once the columns are big enough that not growing them beats the
  11768. // per-token sync, and a page that never reaches it must not carry the cost.
  11769. // Chosen so the streamed cap below stays under `_COLUMN_SHRINK_CAPACITY`,
  11770. // keeping a streamed parse's columns reusable by the next one.
  11771. const _STREAM_MIN_NODES = 49152;
  11772. /**
  11773. * Whether completed subtrees may be visited and recycled at this point in the
  11774. * parse. An empty active-formatting-elements list is the load-bearing check:
  11775. * the adoption agency only moves elements drawn from `open` / `afe` and the
  11776. * direct children of an element that is still open, and table markers live in
  11777. * `afe` too, so with `afe` empty neither it nor foster parenting can reach a
  11778. * node that was already flushed. The rest keep node ids that outlive the open
  11779. * stack out of the recycled range — template contents, pending table text, the
  11780. * `<selectedcontent>` post-pass, the form pointer — and `MODE_IN_BODY` covers
  11781. * the head, which "after head" reopens to insert into.
  11782. * @returns {boolean} true when flushing is safe
  11783. */
  11784. const _canFlush = () =>
  11785. !_streamHalted &&
  11786. afe.length === 0 &&
  11787. templateModes.length === 0 &&
  11788. pendingTableChars === null &&
  11789. !sawSelectedContent &&
  11790. mode === MODE_IN_BODY &&
  11791. open.length !== 0 &&
  11792. (form === 0 || open.includes(form));
  11793. /**
  11794. * Depth-first walk of a whole run of `container`'s children, from `first` up to
  11795. * (not including) `stop` (0 = to the end). One loop for the entire run instead
  11796. * of a call per child — sibling-heavy markup flushes hundreds at a time.
  11797. * @param {HtmlNodeRef} container parent the run hangs off
  11798. * @param {HtmlNodeRef} first first child of the run
  11799. * @param {HtmlNodeRef} stop child to stop before (0 = end of the run)
  11800. */
  11801. const _walkRun = (container, first, stop) => {
  11802. let node = first;
  11803. descend: for (;;) {
  11804. const descending = _enterNode(node);
  11805. // Printing tracks the open path so a node can emit its own pieces; the
  11806. // streamed walk-only runs never print, so they never pay for it.
  11807. if (_writer !== undefined) _openStreamed(node, _printDepth, descending);
  11808. if (descending) {
  11809. const inner = _firstInner(node);
  11810. if (inner !== 0) {
  11811. node = inner;
  11812. _printDepth++;
  11813. continue;
  11814. }
  11815. }
  11816. for (;;) {
  11817. _exitNode(node);
  11818. const parent = _nodeParents[node];
  11819. // Back at the run's own level: step to the next sibling, or stop.
  11820. if (parent === container && _templateContentOf(container) !== node) {
  11821. const next = _nodeNextSiblings[node];
  11822. if (next === 0 || next === stop) return;
  11823. node = next;
  11824. continue descend;
  11825. }
  11826. // Out of a template's content fragment: the element's children follow —
  11827. // they stand in for it, so they share its depth.
  11828. if (_templateContentOf(parent) === node) {
  11829. const inner = _nodeFirstChildren[parent];
  11830. if (inner !== 0) {
  11831. node = inner;
  11832. continue descend;
  11833. }
  11834. } else {
  11835. const next = _nodeNextSiblings[node];
  11836. if (next !== 0) {
  11837. node = next;
  11838. continue descend;
  11839. }
  11840. }
  11841. node = parent;
  11842. _printDepth--;
  11843. }
  11844. }
  11845. };
  11846. /**
  11847. * Rewind the node allocator to `to`, never past the deepest open element. Every
  11848. * still-open element is live and its id must survive, so a flush that empties a
  11849. * parent may not reclaim ids a deeper open element already took.
  11850. * @param {number} to id to rewind to
  11851. */
  11852. const _streamRewind = (to) => {
  11853. const top = open.length !== 0 ? open[open.length - 1] : 0;
  11854. if (to >= top && to < _nodeCount) {
  11855. if (_nodeCount > _nodeHighWater) _nodeHighWater = _nodeCount;
  11856. _nodeCount = to;
  11857. }
  11858. };
  11859. /**
  11860. * Visit and recycle `parent`'s completed children — everything before
  11861. * `openChild`, the child still on the open stack (0 = none, i.e. `parent` is
  11862. * the deepest open element). A trailing text node is held back:
  11863. * `appendTo` merges following text into the last child, so flushing it early
  11864. * would split one text node in two.
  11865. * @param {HtmlNodeRef} parent open element to flush under
  11866. * @param {HtmlElement} openChild still-open child to stop before (0 = none)
  11867. * @param {boolean} visit false to recycle without visiting (under `skipChildren`)
  11868. * @param {boolean} hold true while `parent` can still receive text, so a
  11869. * trailing text node stays put; false once it is closing (nothing follows)
  11870. */
  11871. const _streamFlush = (parent, openChild, visit, hold) => {
  11872. const container = effParent(parent);
  11873. const first = _nodeFirstChildren[container];
  11874. if (first === 0 || first === openChild) return;
  11875. // Where the run ends: before the still-open child, and before a trailing text
  11876. // node while `parent` can still receive text (`appendTo` merges into the last
  11877. // child, so flushing it early would split one text node in two).
  11878. let child = openChild;
  11879. if (hold && openChild === 0) {
  11880. const lastChild = _nodeLastChildren[container];
  11881. if (lastChild !== 0 && _nodeTypes[lastChild] === NodeType.Text) {
  11882. child = lastChild;
  11883. }
  11884. }
  11885. if (first === child) return;
  11886. if (visit) _walkRun(container, first, child);
  11887. _nodeFirstChildren[container] = child;
  11888. if (child === 0) _nodeLastChildren[container] = 0;
  11889. // Ids are bump-allocated in tree order, so the visited run and its
  11890. // descendants fill `[parent + 1, _nodeCount]` whenever nothing below
  11891. // `parent` is still open — rewind the allocator over them.
  11892. if (openChild !== 0 || container !== parent) return;
  11893. if (child === 0) {
  11894. _streamRewind(parent);
  11895. }
  11896. };
  11897. /**
  11898. * Detach a closed element from its parent, releasing its id when it was the
  11899. * parent's last remaining child. Earlier siblings are always flushed before an
  11900. * element is entered, so it is the first child left.
  11901. * @param {HtmlElement} el closed element
  11902. */
  11903. const _streamDetach = (el) => {
  11904. const parent = _nodeParents[el];
  11905. const next = _nodeNextSiblings[el];
  11906. if (parent !== 0) {
  11907. _nodeFirstChildren[parent] = next;
  11908. if (next === 0) _nodeLastChildren[parent] = 0;
  11909. }
  11910. _nodeNextSiblings[el] = 0;
  11911. _nodeParents[el] = 0;
  11912. if (next === 0 && _nodeCount === el) _streamRewind(el - 1);
  11913. };
  11914. /**
  11915. * Bring the streamed walk up to date with the tree built so far: `exit` the
  11916. * elements that closed, `enter` the ones that opened, and flush the completed
  11917. * subtrees in between. Called once per token but does nothing unless
  11918. * `_canFlush()` holds, so an unsafe stretch (incorrectly nested formatting elements,
  11919. * tables, templates) simply buffers and the next catch-up sees the final tree.
  11920. */
  11921. /**
  11922. * Index up to which `_entered` still matches the open stack. Pushes and pops
  11923. * only ever diverge in a suffix; a mid-stack `open.splice` sets `_openSpliced`
  11924. * and forces the full scan.
  11925. * @returns {number} length of the common prefix
  11926. */
  11927. const _enteredPrefix = () => {
  11928. if (_openSpliced) {
  11929. _openSpliced = false;
  11930. let i = 0;
  11931. while (i < _entered.length && i < open.length && _entered[i] === open[i]) {
  11932. i++;
  11933. }
  11934. return i;
  11935. }
  11936. let i = _entered.length < open.length ? _entered.length : open.length;
  11937. while (i > 0 && _entered[i - 1] !== open[i - 1]) i--;
  11938. return i;
  11939. };
  11940. const _streamSync = () => {
  11941. const entered = _entered.length;
  11942. const last = entered !== 0 ? _entered[entered - 1] : 0;
  11943. // Still nothing to do: the entered prefix matches the open stack and too
  11944. // little has piled up under it to be worth a flush.
  11945. if (
  11946. !_openSpliced &&
  11947. open.length >= entered &&
  11948. (entered === 0 || open[entered - 1] === last) &&
  11949. _nodeCount - last < _FLUSH_BATCH
  11950. ) {
  11951. return;
  11952. }
  11953. if (!_canFlush()) return;
  11954. let i = _enteredPrefix();
  11955. // Close: everything entered below the divergence has finished. A descendant
  11956. // of a `skipChildren()` element was tracked without being entered, so it must
  11957. // not exit either — the buffered walk fires `exit` for the skipped element
  11958. // itself and nothing below it.
  11959. for (let k = _entered.length - 1; k >= i; k--) {
  11960. const el = _entered[k];
  11961. const belowSkip = _skipFrom !== -1 && k > _skipFrom;
  11962. _streamFlush(el, 0, _skipFrom === -1 || k < _skipFrom, false);
  11963. if (!belowSkip) _exitNode(el);
  11964. if (_skipFrom === k) _skipFrom = -1;
  11965. _streamDetach(el);
  11966. }
  11967. _entered.length = i;
  11968. // Open: enter the elements now needed, flushing each level's finished
  11969. // children before descending past them.
  11970. for (; i < open.length; i++) {
  11971. const el = open[i];
  11972. const parent = _nodeParents[el];
  11973. const suppressed = _skipFrom !== -1;
  11974. if (parent !== 0) _streamFlush(parent, el, !suppressed, false);
  11975. if (!suppressed && !_enterNode(el)) _skipFrom = i;
  11976. _entered.push(el);
  11977. }
  11978. // Finally the current insertion point's own finished children.
  11979. _streamFlush(open[open.length - 1], 0, _skipFrom === -1, true);
  11980. };
  11981. /**
  11982. * Finish a streamed walk at EOF: close everything still open (their never-
  11983. * entered descendants are walked as completed children), then the root.
  11984. * @param {HtmlNodeRef} root document / fragment root
  11985. */
  11986. const _streamFinish = (root) => {
  11987. for (let k = _entered.length - 1; k >= 0; k--) {
  11988. const el = _entered[k];
  11989. const belowSkip = _skipFrom !== -1 && k > _skipFrom;
  11990. _streamFlush(el, 0, _skipFrom === -1 || k < _skipFrom, false);
  11991. if (!belowSkip) _exitNode(el);
  11992. if (_skipFrom === k) _skipFrom = -1;
  11993. _streamDetach(el);
  11994. }
  11995. _entered.length = 0;
  11996. _streamFlush(root, 0, true, false);
  11997. _exitNode(root);
  11998. };
  11999. /**
  12000. * The HTML `SourceProcessor` grammar: build the document AST (WHATWG tree
  12001. * construction) and walk it, firing `enter` / `exit` in source order. The root
  12002. * document / fragment node is visited too (with a `null` parent). When `writer`
  12003. * is given the same walk also prints: each node's printer builds its text into
  12004. * `writer` as the node finishes (post-order), and the root's text is taken as the
  12005. * result — one parse, no re-tokenization.
  12006. * @param {string} input source text
  12007. * @param {CompiledVisitorMap} visitors compiled visitor map
  12008. * @param {PrintContext | undefined} writer the print context to build output into, or undefined (walk only)
  12009. * @param {HtmlProcessOptions} options process options
  12010. */
  12011. const grammar = (input, visitors, writer, options) => {
  12012. // The walk's state — the visitors, the print context, the open path, the AST
  12013. // columns — is module-scoped so the tokenizer's call sites stay monomorphic,
  12014. // which means one parse at a time: a visitor that started another would take
  12015. // this one's state over and release its columns on the way out. Nesting the
  12016. // *other* language is fine (a `<style>`'s CSS has its own module state), and
  12017. // an `<iframe srcdoc>` is a separate module webpack parses on its own.
  12018. if (_parsing) {
  12019. throw new Error(
  12020. "html syntax: process() is already running; a visitor cannot start another HTML parse"
  12021. );
  12022. }
  12023. _parsing = true;
  12024. _visitors = visitors;
  12025. _writer = writer;
  12026. // Set here, not in `printer`: a tag streams out before the first `printer`
  12027. // call, so these must be known from the start of the walk.
  12028. const redundant =
  12029. writer === undefined ? undefined : writer.options.removeRedundantAttributes;
  12030. _removeRedundantAttributes =
  12031. redundant === true
  12032. ? "smart"
  12033. : redundant === false || redundant === undefined
  12034. ? "none"
  12035. : redundant;
  12036. _removeEmptyAttributes =
  12037. writer !== undefined && writer.options.removeEmptyAttributes === true;
  12038. _removeEmptyElements =
  12039. writer !== undefined && writer.options.removeEmptyElements === true;
  12040. // With the rest rather than in `printer`: they are the print's, not the
  12041. // node's, and that runs once per node.
  12042. _cssEnvironment =
  12043. writer === undefined ? undefined : writer.options.environment;
  12044. _cssConvertLengthUnits =
  12045. writer !== undefined && writer.options.convertLengthUnits === true;
  12046. _cssRewriteCustomProperties =
  12047. writer !== undefined && writer.options.rewriteCustomProperties === true;
  12048. _cssTransforms =
  12049. writer === undefined ? undefined : writer.options.cssTransforms;
  12050. _transforms = _transformsFrom(
  12051. writer === undefined ? undefined : writer.options.transforms
  12052. );
  12053. _commentsKept = _keptComments(_transforms.comments);
  12054. // With the rest, not in `printer`: a tag streams out before the first
  12055. // `printer` call, and both the optional-end-tag tests and
  12056. // `removeEmptyElements` ask what this tier does with a whitespace node.
  12057. const collapse =
  12058. writer === undefined ? undefined : writer.options.collapseWhitespace;
  12059. _collapseWhitespace =
  12060. collapse === true
  12061. ? "conservative"
  12062. : collapse === false || collapse === undefined
  12063. ? "none"
  12064. : collapse;
  12065. _minifying = writer !== undefined && writer.options.mode === "minify";
  12066. _renderEmbeddedSource =
  12067. writer === undefined ? undefined : writer.options.renderEmbeddedSource;
  12068. _deferEmbeddedSource =
  12069. writer === undefined ? undefined : writer.options.deferEmbeddedSource;
  12070. _deferSrcdoc = writer === undefined || writer.options.deferSrcdoc !== false;
  12071. _sortAttributes =
  12072. writer !== undefined && writer.options.sortAttributes === true;
  12073. _sortTokenLists =
  12074. writer !== undefined && writer.options.sortTokenLists === true;
  12075. _mergeStyles = writer !== undefined && writer.options.mergeStyles === true;
  12076. // Node ids are per-parse, so a set kept from the last one would name this
  12077. // one's nodes. Dropped rather than cleared: most documents never fill it.
  12078. _absorbedStyles = null;
  12079. // Counted from the parse this call is about to run, so it cannot be kept.
  12080. _attributeFrequencies = null;
  12081. _attributeRanks = null;
  12082. const implied =
  12083. writer === undefined ? false : writer.options.removeImpliedTags;
  12084. _removeImpliedTags =
  12085. implied === undefined || implied === "smart"
  12086. ? "smart"
  12087. : implied === false
  12088. ? "none"
  12089. : "all";
  12090. // Printing holds every node's text until its parent prints, so only
  12091. // walk-only passes stream; the printing path builds the tree and walks it.
  12092. _streamEligible = writer === undefined;
  12093. _streaming = false;
  12094. _printDepth = 0;
  12095. try {
  12096. const root = parseHtml(input, 0, options);
  12097. let digest = "";
  12098. if (writer !== undefined) {
  12099. if (_fosteredLog !== null) _settleFosteredRuns(A);
  12100. // A shape no arrangement of tags reaches, or foster parenting — whose repair
  12101. // is checked, not trusted. No real document reaches either.
  12102. if (_printFromSource || _fosteredLog !== null) digest = _digest(root);
  12103. _walkRun(_nodeParents[root], root, _nodeNextSiblings[root]);
  12104. // A single root: take its text as the output (the printer read every node's
  12105. // source into the store during the walk, so this needs no source). A root
  12106. // that printed in pieces has already emitted all of it.
  12107. if (_printOpen[0] === 1) writer.dropStore();
  12108. else writer.take(root);
  12109. // Print, then read the output back: the source always parses to its own
  12110. // tree, so it stands in when the printed form does not.
  12111. if (digest !== "") {
  12112. const printed = writer.result();
  12113. if (_digest(parseHtml(printed, 0, options)) !== digest) {
  12114. writer.replaceAll(input);
  12115. }
  12116. }
  12117. }
  12118. } finally {
  12119. // Each held write keeps its offered source — a `<style>`, a `<script>`, a
  12120. // whole `srcdoc` document — and a closure over it, so both go with the parse.
  12121. _renderEmbeddedSource = undefined;
  12122. _deferEmbeddedSource = undefined;
  12123. _parsing = false;
  12124. _removeImpliedTags = "smart";
  12125. _transforms = _DEFAULT_TRANSFORMS;
  12126. _minifying = false;
  12127. // Keyed by this parse's interned names, so it must not outlive it.
  12128. _attributeFrequencies = null;
  12129. _attributeRanks = null;
  12130. // Keyed by this print's options, and holding slices of its input.
  12131. _styleAttributeCache.clear();
  12132. _emptyElements.clear();
  12133. _streamEligible = false;
  12134. _streaming = false;
  12135. _docSkipped = false;
  12136. // Held across the walk, so they outlive it the way the visitors do.
  12137. _printOpen.length = 0;
  12138. _printClose.length = 0;
  12139. _printPendingNames.length = 0;
  12140. _printPendingSlot.length = 0;
  12141. // Hoisting these to module scope outlives the call, so drop the caller's
  12142. // visitors and print context here — they used to fall away with `grammar`.
  12143. _visitors = EMPTY_VISITORS;
  12144. _writer = undefined;
  12145. // The walk consumed the tree: release the side arrays' heap references so
  12146. // the reused columns don't pin this parse's strings until the next parse.
  12147. _releaseAstColumns();
  12148. }
  12149. };
  12150. /**
  12151. * The generic visitor coordinator (`util/SourceProcessor`) bound to the HTML
  12152. * `grammar`. Babel-style usage:
  12153. *
  12154. * ```
  12155. * new SourceProcessor().use({ [NodeType.Element]: (path) => {}, [NodeType.Comment]: { enter, exit } }).process(source, { skip });
  12156. * ```
  12157. * @experimental exposed as `webpack.html.syntax.SourceProcessor`; unstable API
  12158. * @extends {GenericSourceProcessor<HtmlPath, HtmlNodeRef, HtmlProcessOptions>}
  12159. */
  12160. class SourceProcessor extends GenericSourceProcessor {
  12161. constructor() {
  12162. super(grammar, printer);
  12163. }
  12164. }
  12165. /** @typedef {[string, number, number]} ParsedSource */
  12166. // `parseSrcset` is a direct implementation of the WHATWG "parse a srcset
  12167. // attribute" algorithm; it lives here with the other spec-level HTML parsing
  12168. // so it can move into the WASM parser alongside the tokenizer in the future.
  12169. const COMMA = ",".charCodeAt(0);
  12170. const LEFT_PARENTHESIS = "(".charCodeAt(0);
  12171. const RIGHT_PARENTHESIS = ")".charCodeAt(0);
  12172. const SMALL_LETTER_W = "w".charCodeAt(0);
  12173. const SMALL_LETTER_X = "x".charCodeAt(0);
  12174. const SMALL_LETTER_H = "h".charCodeAt(0);
  12175. // (Don't use \s, to avoid matching non-breaking space)
  12176. // Sticky so `collectCharacters` matches at an offset without slicing the
  12177. // whole remaining input per call (which made srcset parsing quadratic).
  12178. // eslint-disable-next-line no-control-regex
  12179. const LEADING_SPACES_REGEXP = /[ \t\n\r\u000C]+/y;
  12180. // eslint-disable-next-line no-control-regex
  12181. const LEADING_COMMAS_OR_SPACES_REGEXP = /[, \t\n\r\u000C]+/y;
  12182. // eslint-disable-next-line no-control-regex
  12183. const LEADING_NOT_SPACES = /[^ \t\n\r\u000C]+/y;
  12184. const TRAILING_COMMAS_REGEXP = /[,]+$/;
  12185. const NON_NEGATIVE_INTEGER_REGEXP = /^\d+$/;
  12186. // ( Positive or negative or unsigned integers or decimals, without or without exponents.
  12187. // Must include at least one digit.
  12188. // According to spec tests any decimal point must be followed by a digit.
  12189. // No leading plus sign is allowed.)
  12190. // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-floating-point-number
  12191. const FLOATING_POINT_REGEXP =
  12192. /^-?(?:[0-9]+|[0-9]*\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/;
  12193. /**
  12194. * @param {string} input input
  12195. * @returns {ParsedSource[]} parsed srcset
  12196. */
  12197. const parseSrcset = (input) => {
  12198. // 1. Let input be the value passed to this algorithm.
  12199. const inputLength = input.length;
  12200. /** @type {string | undefined} */
  12201. let url;
  12202. /** @type {string[]} */
  12203. let descriptors;
  12204. /** @type {number} */
  12205. let descriptorStart;
  12206. /** @type {string} */
  12207. let state;
  12208. /** @type {number} */
  12209. let charCode;
  12210. /** @type {number} */
  12211. let position = 0;
  12212. /** @type {number} */
  12213. let start;
  12214. /** @type {[string, number, number][]} */
  12215. const candidates = [];
  12216. /**
  12217. * @param {RegExp} regExp sticky reg exp to collect characters
  12218. * @returns {string | undefined} characters
  12219. */
  12220. function collectCharacters(regExp) {
  12221. regExp.lastIndex = Math.max(0, position);
  12222. const match = regExp.exec(input);
  12223. if (match) {
  12224. const [chars] = match;
  12225. position += chars.length;
  12226. return chars;
  12227. }
  12228. }
  12229. /**
  12230. * @returns {void}
  12231. */
  12232. function parseDescriptors() {
  12233. // 9. Descriptor parser: Let error be no.
  12234. let pError = false;
  12235. // 10. Let width be absent.
  12236. // 11. Let density be absent.
  12237. // 12. Let future-compat-h be absent. (We're implementing it now as h)
  12238. /** @type {number | undefined} */
  12239. let width;
  12240. /** @type {number | undefined} */
  12241. let density;
  12242. /** @type {number | undefined} */
  12243. let height;
  12244. /** @type {string | undefined} */
  12245. let desc;
  12246. // 13. For each descriptor in descriptors, run the appropriate set of steps
  12247. // from the following list:
  12248. for (let i = 0; i < descriptors.length; i++) {
  12249. desc = descriptors[i];
  12250. const lastChar = desc[desc.length - 1].charCodeAt(0);
  12251. const value = desc.slice(0, Math.max(0, desc.length - 1));
  12252. // If the descriptor consists of a valid non-negative integer followed by
  12253. // a U+0077 LATIN SMALL LETTER W character
  12254. if (
  12255. NON_NEGATIVE_INTEGER_REGEXP.test(value) &&
  12256. lastChar === SMALL_LETTER_W
  12257. ) {
  12258. // If width and density are not both absent, then let error be yes.
  12259. if (width || density) {
  12260. pError = true;
  12261. }
  12262. const intVal = Number.parseInt(value, 10);
  12263. // Apply the rules for parsing non-negative integers to the descriptor.
  12264. // If the result is zero, let error be yes.
  12265. // Otherwise, let width be the result.
  12266. if (intVal === 0) {
  12267. pError = true;
  12268. } else {
  12269. width = intVal;
  12270. }
  12271. }
  12272. // If the descriptor consists of a valid floating-point number followed by
  12273. // a U+0078 LATIN SMALL LETTER X character
  12274. else if (
  12275. FLOATING_POINT_REGEXP.test(value) &&
  12276. lastChar === SMALL_LETTER_X
  12277. ) {
  12278. // If width, density and future-compat-h are not all absent, then let error
  12279. // be yes.
  12280. if (width || density || height) {
  12281. pError = true;
  12282. }
  12283. const floatVal = Number.parseFloat(value);
  12284. // Apply the rules for parsing floating-point number values to the descriptor.
  12285. // If the result is less than zero, let error be yes. Otherwise, let density
  12286. // be the result.
  12287. if (floatVal < 0) {
  12288. pError = true;
  12289. } else {
  12290. density = floatVal;
  12291. }
  12292. }
  12293. // If the descriptor consists of a valid non-negative integer followed by
  12294. // a U+0068 LATIN SMALL LETTER H character
  12295. else if (
  12296. NON_NEGATIVE_INTEGER_REGEXP.test(value) &&
  12297. lastChar === SMALL_LETTER_H
  12298. ) {
  12299. // If height and density are not both absent, then let error be yes.
  12300. if (height || density) {
  12301. pError = true;
  12302. }
  12303. const intVal = Number.parseInt(value, 10);
  12304. // Apply the rules for parsing non-negative integers to the descriptor.
  12305. // If the result is zero, let error be yes. Otherwise, let future-compat-h
  12306. // be the result.
  12307. if (intVal === 0) {
  12308. pError = true;
  12309. } else {
  12310. height = intVal;
  12311. }
  12312. // Anything else, Let error be yes.
  12313. } else {
  12314. pError = true;
  12315. }
  12316. }
  12317. // 15. If error is still no, then append a new image source to candidates whose
  12318. // URL is url, associated with a width width if not absent and a pixel
  12319. // density density if not absent. Otherwise, there is a parse error.
  12320. if (!pError) {
  12321. candidates.push([
  12322. /** @type {string} */ (url),
  12323. start,
  12324. start + /** @type {string} */ (url).length
  12325. ]);
  12326. } else {
  12327. throw new Error(
  12328. `Invalid srcset descriptor found in '${input}' at '${desc}'`
  12329. );
  12330. }
  12331. }
  12332. /**
  12333. * @returns {void}
  12334. */
  12335. function tokenizeDescriptor() {
  12336. // 8.1. Descriptor tokenizer: Skip whitespace
  12337. collectCharacters(LEADING_SPACES_REGEXP);
  12338. // 8.2. Let current descriptor be the empty string.
  12339. // (Tracked as a start offset, `-1` = empty; sliced once per descriptor.)
  12340. descriptorStart = -1;
  12341. // 8.3. Let state be in descriptor.
  12342. state = "in descriptor";
  12343. while (true) {
  12344. // 8.4. Let charCode be the character at position.
  12345. charCode = input.charCodeAt(position);
  12346. // Do the following depending on the value of state.
  12347. // For the purpose of this step, "EOF" is a special character representing
  12348. // that position is past the end of input.
  12349. // In descriptor
  12350. if (state === "in descriptor") {
  12351. // Do the following, depending on the value of charCode:
  12352. // Space character
  12353. // If current descriptor is not empty, append current descriptor to
  12354. // descriptors and let current descriptor be the empty string.
  12355. // Set state to after descriptor.
  12356. if (isSpace(charCode)) {
  12357. if (descriptorStart !== -1) {
  12358. descriptors.push(input.slice(descriptorStart, position));
  12359. descriptorStart = -1;
  12360. state = "after descriptor";
  12361. }
  12362. }
  12363. // U+002C COMMA (,)
  12364. // Advance position to the next character in input. If current descriptor
  12365. // is not empty, append current descriptor to descriptors. Jump to the step
  12366. // labeled descriptor parser.
  12367. else if (charCode === COMMA) {
  12368. position += 1;
  12369. if (descriptorStart !== -1) {
  12370. descriptors.push(input.slice(descriptorStart, position - 1));
  12371. }
  12372. parseDescriptors();
  12373. return;
  12374. }
  12375. // U+0028 LEFT PARENTHESIS (()
  12376. // Append charCode to current descriptor. Set state to in parens.
  12377. else if (charCode === LEFT_PARENTHESIS) {
  12378. if (descriptorStart === -1) descriptorStart = position;
  12379. state = "in parens";
  12380. }
  12381. // EOF
  12382. // If current descriptor is not empty, append current descriptor to
  12383. // descriptors. Jump to the step labeled descriptor parser.
  12384. else if (Number.isNaN(charCode)) {
  12385. if (descriptorStart !== -1) {
  12386. descriptors.push(input.slice(descriptorStart, position));
  12387. }
  12388. parseDescriptors();
  12389. return;
  12390. // Anything else
  12391. // Append charCode to current descriptor.
  12392. } else if (descriptorStart === -1) {
  12393. descriptorStart = position;
  12394. }
  12395. }
  12396. // In parens
  12397. else if (state === "in parens") {
  12398. // U+0029 RIGHT PARENTHESIS ())
  12399. // Append charCode to current descriptor. Set state to in descriptor.
  12400. if (charCode === RIGHT_PARENTHESIS) {
  12401. state = "in descriptor";
  12402. }
  12403. // EOF
  12404. // Append current descriptor to descriptors. Jump to the step labeled
  12405. // descriptor parser.
  12406. else if (Number.isNaN(charCode)) {
  12407. descriptors.push(input.slice(descriptorStart, position));
  12408. parseDescriptors();
  12409. return;
  12410. }
  12411. // Anything else
  12412. // Append charCode to current descriptor. (Covered by the tracked range.)
  12413. }
  12414. // After descriptor
  12415. else if (state === "after descriptor") {
  12416. // Do the following, depending on the value of charCode:
  12417. if (isSpace(charCode)) {
  12418. // Space character: Stay in this state.
  12419. }
  12420. // EOF: Jump to the step labeled descriptor parser.
  12421. else if (Number.isNaN(charCode)) {
  12422. parseDescriptors();
  12423. return;
  12424. }
  12425. // Anything else
  12426. // Set state to in descriptor. Set position to the previous character in input.
  12427. else {
  12428. state = "in descriptor";
  12429. position -= 1;
  12430. }
  12431. }
  12432. // Advance position to the next character in input.
  12433. position += 1;
  12434. }
  12435. }
  12436. // 3. Let candidates be an initially empty source set.
  12437. // const candidates = []; // Moved to top
  12438. // 4. Splitting loop: Collect a sequence of characters that are space
  12439. // characters or U+002C COMMA characters. If any U+002C COMMA characters
  12440. // were collected, that is a parse error.
  12441. while (true) {
  12442. collectCharacters(LEADING_COMMAS_OR_SPACES_REGEXP);
  12443. // 5. If position is past the end of input, return candidates and abort these steps.
  12444. if (position >= inputLength) {
  12445. if (candidates.length === 0) {
  12446. throw new Error("Must contain one or more image candidate strings");
  12447. }
  12448. // (we're done, this is the sole return path)
  12449. return candidates;
  12450. }
  12451. // 6. Collect a sequence of characters that are not space characters,
  12452. // and let that be url.
  12453. start = position;
  12454. url = collectCharacters(LEADING_NOT_SPACES);
  12455. // 7. Let descriptors be a new empty list.
  12456. descriptors = [];
  12457. // 8. If url ends with a U+002C COMMA character (,), follow these sub steps:
  12458. // (1). Remove all trailing U+002C COMMA characters from url. If this removed
  12459. // more than one character, that is a parse error.
  12460. if (url && url.charCodeAt(url.length - 1) === COMMA) {
  12461. url = url.replace(TRAILING_COMMAS_REGEXP, "");
  12462. // (Jump ahead to step 9 to skip tokenization and just push the candidate).
  12463. parseDescriptors();
  12464. }
  12465. // Otherwise, follow these sub steps:
  12466. else {
  12467. tokenizeDescriptor();
  12468. }
  12469. // 16. Return to the step labeled splitting loop.
  12470. }
  12471. };
  12472. // The spec's "URL-potentially-surrounded-by-spaces" cleanup: C0 controls,
  12473. // DEL..C1 and U+00A0 are stripped from the value after the whitespace trim.
  12474. // eslint-disable-next-line no-control-regex
  12475. const IGNORE_CHARS_REGEXP = /[\u0000-\u001F\u007F-\u009F\u00A0]/g;
  12476. /**
  12477. * Parse a `src`-like URL attribute value: trim ASCII whitespace / U+00A0 from
  12478. * both ends (offsets preserved for source rewriting), then strip ignorable
  12479. * control characters. Throws when nothing remains.
  12480. * @param {string} input attribute value
  12481. * @returns {ParsedSource[]} parsed src
  12482. */
  12483. const parseSrc = (input) => {
  12484. const len = input.length;
  12485. if (len === 0) throw new Error("Must be non-empty");
  12486. let start = 0;
  12487. let end = len;
  12488. while (start < end) {
  12489. const code = input.charCodeAt(start);
  12490. if (code > 32 && code !== 160) break;
  12491. start++;
  12492. }
  12493. if (start === end) throw new Error("Must be non-empty");
  12494. while (end > start) {
  12495. const code = input.charCodeAt(end - 1);
  12496. if (code > 32 && code !== 160) break;
  12497. end--;
  12498. }
  12499. let value = input.slice(start, end);
  12500. if (IGNORE_CHARS_REGEXP.test(value)) {
  12501. value = value.replace(IGNORE_CHARS_REGEXP, "");
  12502. if (value.length === 0) throw new Error("Must be non-empty");
  12503. }
  12504. return [[value, start, end]];
  12505. };
  12506. /**
  12507. * Extracts the `icon-uri` value of an `msapplication-task` meta content
  12508. * (`name=…;action-uri=…;icon-uri=…`) — the other parts are page URLs, not assets.
  12509. * @param {string} input input
  12510. * @returns {ParsedSource[]} parsed icon-uri
  12511. */
  12512. const parseMsapplicationTask = (input) => {
  12513. const len = input.length;
  12514. let pos = 0;
  12515. while (pos < len) {
  12516. let sep = input.indexOf(";", pos);
  12517. if (sep === -1) sep = len;
  12518. const eq = input.indexOf("=", pos);
  12519. if (eq !== -1 && eq < sep) {
  12520. const key = input.slice(pos, eq).trim().toLowerCase();
  12521. if (key === "icon-uri") {
  12522. let start = eq + 1;
  12523. let end = sep;
  12524. while (start < end && isSpace(input.charCodeAt(start))) {
  12525. start++;
  12526. }
  12527. while (end > start && isSpace(input.charCodeAt(end - 1))) {
  12528. end--;
  12529. }
  12530. if (start === end) return [];
  12531. return [[input.slice(start, end), start, end]];
  12532. }
  12533. }
  12534. pos = sep + 1;
  12535. }
  12536. return [];
  12537. };
  12538. // CSS syntax is loaded on first `parseCssUrls` call, not at module load —
  12539. // `HtmlGenerator` deliberately defers it the same way, and most documents
  12540. // never carry a URL-bearing SVG presentation attribute.
  12541. /** @type {typeof import("../css/syntax") | undefined} */
  12542. let _cssSyntax;
  12543. /**
  12544. * Extracts `url(...)` references from a CSS value (an SVG presentation
  12545. * attribute). Reuses webpack's CSS lexer so quoting/escaping match the CSS
  12546. * spec; returns the `parseSrc` shape so the shared emit path maps and rewrites
  12547. * the spans. Unquoted `url(path)` rewrites the content span; quoted
  12548. * `url("path")` rewrites the inner string span (quotes preserved).
  12549. * @param {string} input attribute value (a CSS component-value list)
  12550. * @returns {ParsedSource[]} url references
  12551. */
  12552. const parseCssUrls = (input) => {
  12553. const {
  12554. TT_EOF,
  12555. TT_FUNCTION,
  12556. TT_STRING,
  12557. TT_URL,
  12558. TT_WHITESPACE,
  12559. TokenStream,
  12560. equalsLowerCase
  12561. } = _cssSyntax || (_cssSyntax = require("../css/syntax"));
  12562. const ts = new TokenStream(input);
  12563. /** @type {ParsedSource[]} */
  12564. const result = [];
  12565. for (;;) {
  12566. const t = ts.consume();
  12567. if (t.type === TT_EOF) break;
  12568. if (t.type === TT_URL) {
  12569. if (t.contentEnd > t.contentStart) {
  12570. result.push([
  12571. input.slice(t.contentStart, t.contentEnd),
  12572. t.contentStart,
  12573. t.contentEnd
  12574. ]);
  12575. }
  12576. } else if (
  12577. t.type === TT_FUNCTION &&
  12578. equalsLowerCase(input.slice(t.start, t.end - 1), "url")
  12579. ) {
  12580. let s = ts.consume();
  12581. while (s.type === TT_WHITESPACE) s = ts.consume();
  12582. if (s.type === TT_STRING) {
  12583. const quote = input.charCodeAt(s.start);
  12584. const innerStart = s.start + 1;
  12585. // Drop the closing quote, unless the string is unterminated at EOF.
  12586. const innerEnd =
  12587. input.charCodeAt(s.end - 1) === quote ? s.end - 1 : s.end;
  12588. if (innerEnd > innerStart) {
  12589. result.push([
  12590. input.slice(innerStart, innerEnd),
  12591. innerStart,
  12592. innerEnd
  12593. ]);
  12594. }
  12595. }
  12596. }
  12597. }
  12598. return result;
  12599. };
  12600. // Babel's `path.skip()`, children-only: set by `A.skipChildren()` during an
  12601. // `enter` dispatch, consumed by the walk.
  12602. let _walkSkip = false;
  12603. // The walk's current position (`A.node` / `A.parent` read these; module-level
  12604. // so the accessor methods' defaults avoid self-referential `this` typing).
  12605. /** @type {HtmlNodeRef} */
  12606. let _currentNode = 0;
  12607. /** @type {HtmlNodeRef | null} */
  12608. let _currentParent = null;
  12609. /* eslint-disable jsdoc/require-template -- `A` below is the accessor const, not a type parameter */
  12610. /**
  12611. * The HTML path (Babel's `path` shape): the AST accessor with the walk's
  12612. * current position on it — the single argument every visitor receives.
  12613. * @typedef {typeof A} HtmlPath
  12614. */
  12615. /* eslint-enable jsdoc/require-template */
  12616. // AST field-access seam (mirrors the CSS parser's `A`): every AST field a
  12617. // consumer reads goes through one of these accessors, so the node
  12618. // representation can change underneath without touching consumers. `n` is an
  12619. // `HtmlNodeRef`; results are valid until the next `parseHtml` call.
  12620. const A = {
  12621. // === path position (rebound by the walk before every visitor call) ===
  12622. /**
  12623. * @returns {HtmlNodeRef} current node — only valid during a visitor callback
  12624. */
  12625. get node() {
  12626. return _currentNode;
  12627. },
  12628. /**
  12629. * @returns {HtmlNodeRef | null} enclosing node (null = the document root)
  12630. */
  12631. get parent() {
  12632. return _currentParent;
  12633. },
  12634. /** Stop the walk descending into the current node (enter only). */
  12635. skipChildren() {
  12636. _walkSkip = true;
  12637. },
  12638. // === field reads — `n` defaults to the current node ===
  12639. /**
  12640. * @param {HtmlNodeRef=} n node
  12641. * @returns {number} `NodeType`
  12642. */
  12643. type(n = _currentNode) {
  12644. return _nodeTypes[n];
  12645. },
  12646. /**
  12647. * @param {HtmlNodeRef=} n node
  12648. * @returns {number} start offset
  12649. */
  12650. start(n = _currentNode) {
  12651. return _nodeStarts[n];
  12652. },
  12653. /**
  12654. * @param {HtmlNodeRef=} n node
  12655. * @returns {number} end offset
  12656. */
  12657. end(n = _currentNode) {
  12658. return _nodeEnds[n];
  12659. },
  12660. /**
  12661. * Raw source slice `[start, end)` — valid only during the walk (the printer's
  12662. * window), before `parseHtml` releases `_htmlSource`.
  12663. * @param {HtmlNodeRef=} n node
  12664. * @returns {string} raw source slice
  12665. */
  12666. source(n = _currentNode) {
  12667. return _htmlSource.slice(_nodeStarts[n], _nodeEnds[n]);
  12668. },
  12669. /**
  12670. * @param {number} from start offset
  12671. * @param {number} to end offset
  12672. * @returns {string} raw source between the two offsets
  12673. */
  12674. sourceSpanAt(from, to) {
  12675. return _htmlSource.slice(from, to);
  12676. },
  12677. /**
  12678. * @param {HtmlElement=} n element
  12679. * @returns {string} lowercased (foreign-content: adjusted) tag name
  12680. */
  12681. tagName(n = _currentNode) {
  12682. return _nodeStrings[n];
  12683. },
  12684. /**
  12685. * @param {HtmlElement=} n element
  12686. * @returns {number} `NS_*` namespace
  12687. */
  12688. namespace(n = _currentNode) {
  12689. return _nodeFlags[n] & NS_MASK;
  12690. },
  12691. /**
  12692. * @param {HtmlElement=} n element
  12693. * @returns {boolean} true for void elements
  12694. */
  12695. selfClosing(n = _currentNode) {
  12696. return (_nodeFlags[n] & FLAG_SELF_CLOSING) !== 0;
  12697. },
  12698. // materialized attribute list — test/tooling convenience, allocates; the
  12699. // parser reads attributes through the scalar accessors below
  12700. /**
  12701. * @param {HtmlElement=} n element
  12702. * @returns {HtmlAttribute[]} materialized attributes
  12703. */
  12704. attributes(n = _currentNode) {
  12705. const out = [];
  12706. const start = _nodeAttrStarts[n];
  12707. for (let i = start; i < start + _nodeAttrCounts[n]; i++) {
  12708. out.push({
  12709. name: _attrNames[i],
  12710. value: _attrValueOf(i),
  12711. serializedName: _attrSerializedName(i),
  12712. nameStart: _attrNameStarts[i],
  12713. nameEnd: _attrNameEnds[i],
  12714. valueStart: _attrValueStarts[i],
  12715. valueEnd: _attrValueEnds[i]
  12716. });
  12717. }
  12718. return out;
  12719. },
  12720. /**
  12721. * @param {HtmlElement=} n element
  12722. * @returns {number} attribute count
  12723. */
  12724. attributeCount(n = _currentNode) {
  12725. return _nodeAttrCounts[n];
  12726. },
  12727. /**
  12728. * The i-th attribute of an element, as an id for the `attribute*` reads.
  12729. * @param {number} i attribute index
  12730. * @param {HtmlElement=} n element
  12731. * @returns {HtmlAttributeRef} attribute ref
  12732. */
  12733. attributeAt(i, n = _currentNode) {
  12734. return _nodeAttrStarts[n] + i;
  12735. },
  12736. /**
  12737. * Linear lookup by (lowercased) name.
  12738. * @param {string} name attribute name
  12739. * @param {HtmlElement=} n element
  12740. * @returns {HtmlAttributeRef} attribute ref (0 = not present)
  12741. */
  12742. findAttribute(name, n = _currentNode) {
  12743. return _findAttr(_nodeAttrStarts[n], _nodeAttrCounts[n], name);
  12744. },
  12745. /**
  12746. * @param {HtmlAttributeRef} a attribute ref
  12747. * @returns {string} lowercased (foreign-content: adjusted) attribute name
  12748. */
  12749. attributeName(a) {
  12750. return _attrNames[a];
  12751. },
  12752. /**
  12753. * @param {HtmlAttributeRef} a attribute ref
  12754. * @returns {string} raw (undecoded) attribute value ("" when valueless)
  12755. */
  12756. attributeValue(a) {
  12757. return _attrValueOf(a);
  12758. },
  12759. /**
  12760. * @param {HtmlAttributeRef} a attribute ref
  12761. * @returns {number} name start offset
  12762. */
  12763. attributeNameStart(a) {
  12764. return _attrNameStarts[a];
  12765. },
  12766. /**
  12767. * @param {HtmlAttributeRef} a attribute ref
  12768. * @returns {number} name end offset
  12769. */
  12770. attributeNameEnd(a) {
  12771. return _attrNameEnds[a];
  12772. },
  12773. /**
  12774. * @param {HtmlAttributeRef} a attribute ref
  12775. * @returns {number} value start offset (-1 when valueless or on adoption-agency clones)
  12776. */
  12777. attributeValueStart(a) {
  12778. return _attrValueStarts[a];
  12779. },
  12780. /**
  12781. * @param {HtmlAttributeRef} a attribute ref
  12782. * @returns {number} value end offset
  12783. */
  12784. attributeValueEnd(a) {
  12785. return _attrValueEnds[a];
  12786. },
  12787. /**
  12788. * @param {HtmlElement=} n element
  12789. * @returns {number} end offset of the opening tag (after `>`)
  12790. */
  12791. tagEnd(n = _currentNode) {
  12792. return _nodeTagEnds[n];
  12793. },
  12794. /**
  12795. * @param {HtmlElement=} n element
  12796. * @returns {number} end offset of the tag name
  12797. */
  12798. nameEnd(n = _currentNode) {
  12799. return _nodeNameEnds[n];
  12800. },
  12801. /**
  12802. * Raw source of an element's opening tag, `[start, tagEnd)` — attribute quoting
  12803. * / spacing / case preserved byte-for-byte (walk-window only) — or `""` for a
  12804. * parser-inserted element (auto `html`/`head`/`body`/`tbody`, …), which has no
  12805. * real source tag: its offsets are zero-width or borrow the triggering token,
  12806. * so the sliced name doesn't match this element. The empty string lets a printer
  12807. * treat such an element as transparent.
  12808. * @param {HtmlElement=} n element
  12809. * @returns {string} opening-tag source, or `""` when parser-inserted
  12810. */
  12811. /**
  12812. * Whether the source wrote this element's end tag rather than the parser
  12813. * popping it for an implied close. Read back off the range instead of marked
  12814. * during the parse: an element's end spans the token that closed it, so its
  12815. * own end tag is the last thing in it — and only the few elements around a
  12816. * region printed from source ever ask.
  12817. * @param {HtmlElement=} n element
  12818. * @returns {boolean} whether the source wrote its end tag
  12819. */
  12820. sourceClosed(n = _currentNode) {
  12821. const end = _nodeEnds[n];
  12822. if (end === 0 || _htmlSource.charCodeAt(end - 1) !== 62) return false;
  12823. let i = end - 2;
  12824. for (;;) {
  12825. const c = i >= 0 ? _htmlSource.charCodeAt(i) : 0;
  12826. if (c !== 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0c && c !== 0x0d) {
  12827. break;
  12828. }
  12829. i--;
  12830. }
  12831. const name = _tagNameOf(n);
  12832. const nameEnd = i + 1;
  12833. const nameStart = nameEnd - name.length;
  12834. return (
  12835. nameStart >= 2 &&
  12836. _htmlSource.charCodeAt(nameStart - 1) === 47 &&
  12837. _htmlSource.charCodeAt(nameStart - 2) === 60 &&
  12838. rangeEqualsLowerCase(_htmlSource, nameStart, nameEnd, name)
  12839. );
  12840. },
  12841. openTag(n = _currentNode) {
  12842. const start = _nodeStarts[n];
  12843. const nameEnd = _nodeNameEnds[n];
  12844. const stored = _nodeStrings[n];
  12845. // Fold the raw range against the stored name in place — no name slice, no
  12846. // `toLowerCase` pair. An adjusted foreign name (`foreignObject`) carries
  12847. // upper case the fold cannot match, so a miss falls back to the old compare.
  12848. if (rangeEqualsLowerCase(_htmlSource, start + 1, nameEnd, stored)) {
  12849. return _htmlSource.slice(start, _nodeTagEnds[n]);
  12850. }
  12851. const name = _htmlSource.slice(start + 1, nameEnd);
  12852. return name.toLowerCase() === stored.toLowerCase()
  12853. ? _htmlSource.slice(start, _nodeTagEnds[n])
  12854. : "";
  12855. },
  12856. /**
  12857. * An element's end tag, generated as `</name>` from the opening tag's own name
  12858. * (exact source casing, correct for foreign camelCase elements). Generated, not
  12859. * sliced: element `end` offsets don't span the end tag, and an omitted optional
  12860. * end tag (`<li>`, `<p>`, …) still serializes to the same DOM. `""` when the
  12861. * parser inserted the element, as {@link openTag} does — it has no name in the
  12862. * source to echo, and slicing one would spell `</>`.
  12863. * @param {HtmlElement=} n element
  12864. * @returns {string} closing-tag text, or `""` when parser-inserted
  12865. */
  12866. closeTag(n = _currentNode) {
  12867. const nameEnd = _nodeNameEnds[n];
  12868. return nameEnd === 0
  12869. ? ""
  12870. : `</${_htmlSource.slice(_nodeStarts[n] + 1, nameEnd)}>`;
  12871. },
  12872. /**
  12873. * @param {HtmlElement=} n element
  12874. * @returns {number} under `skip.text`, end offset of a raw-text element's body (`tagEnd` when empty)
  12875. */
  12876. contentEnd(n = _currentNode) {
  12877. return (_nodeFlags[n] & FLAG_HAS_TEMPLATE) !== 0
  12878. ? _nodeTagEnds[n]
  12879. : _nodeContentEnds[n];
  12880. },
  12881. /**
  12882. * @param {HtmlElement=} n element
  12883. * @returns {HtmlDocumentFragment} `<template>` content fragment (0 = none)
  12884. */
  12885. templateContent(n = _currentNode) {
  12886. return _templateContentOf(n);
  12887. },
  12888. /**
  12889. * @param {HtmlText | HtmlComment | HtmlProcessingInstruction=} n text / comment / processing instruction node
  12890. * @returns {string} decoded text / comment / processing instruction data
  12891. */
  12892. data(n = _currentNode) {
  12893. return _nodeStrings[n];
  12894. },
  12895. /**
  12896. * @param {HtmlProcessingInstruction=} n processing instruction node
  12897. * @returns {string} processing instruction target
  12898. */
  12899. piTarget(n = _currentNode) {
  12900. return _piTargets.get(n) || "";
  12901. },
  12902. /**
  12903. * @param {HtmlDoctype=} n doctype node
  12904. * @returns {string} doctype name
  12905. */
  12906. doctypeName(n = _currentNode) {
  12907. return _nodeStrings[n];
  12908. },
  12909. // The doctype ids are per-parse scalars (a document has at most one
  12910. // doctype node); the node parameter is accepted for call-shape uniformity.
  12911. /**
  12912. * @param {HtmlDoctype=} _n doctype node
  12913. * @returns {string | null} doctype public id
  12914. */
  12915. doctypePublicId(_n) {
  12916. return _doctypePublicId;
  12917. },
  12918. /**
  12919. * @param {HtmlDoctype=} _n doctype node
  12920. * @returns {string | null} doctype system id
  12921. */
  12922. doctypeSystemId(_n) {
  12923. return _doctypeSystemId;
  12924. },
  12925. // === tree links (0 = none) ===
  12926. /**
  12927. * @param {HtmlNodeRef=} n node
  12928. * @returns {HtmlNodeRef} first child
  12929. */
  12930. firstChild(n = _currentNode) {
  12931. return _nodeFirstChildren[n];
  12932. },
  12933. /**
  12934. * @param {HtmlNodeRef=} n node
  12935. * @returns {HtmlNodeRef} next sibling
  12936. */
  12937. nextSibling(n = _currentNode) {
  12938. return _nodeNextSiblings[n];
  12939. },
  12940. /**
  12941. * @param {HtmlNodeRef=} n node
  12942. * @returns {HtmlNodeRef} parent node (a `<template>`'s content links to its fragment)
  12943. */
  12944. parentOf(n = _currentNode) {
  12945. return _nodeParents[n];
  12946. },
  12947. /**
  12948. * @param {HtmlNodeRef=} n node
  12949. * @returns {HtmlNodeRef[]} materialized child list — test/tooling convenience, allocates
  12950. */
  12951. children(n = _currentNode) {
  12952. const out = [];
  12953. for (let c = _nodeFirstChildren[n]; c !== 0; c = _nodeNextSiblings[c]) {
  12954. out.push(c);
  12955. }
  12956. return out;
  12957. }
  12958. };
  12959. module.exports.A = A;
  12960. module.exports.EMBEDDED_LANGUAGES = EMBEDDED_LANGUAGES;
  12961. module.exports.NS_HTML = NS_HTML;
  12962. module.exports.NS_MATHML = NS_MATHML;
  12963. module.exports.NS_SVG = NS_SVG;
  12964. module.exports.NodeType = NodeType;
  12965. module.exports.QUOTE_DOUBLE = QUOTE_DOUBLE;
  12966. module.exports.QUOTE_NONE = QUOTE_NONE;
  12967. module.exports.QUOTE_SINGLE = QUOTE_SINGLE;
  12968. // Exposed so HtmlParser can map user-configured (lowercased) tag names to
  12969. // the adjusted camelCase names the AST carries for foreign content.
  12970. module.exports.SVG_TAG_ADJUST = SVG_TAG_ADJUST;
  12971. module.exports.SourceProcessor = SourceProcessor;
  12972. module.exports.askEmbeddedRenderer = askEmbeddedRenderer;
  12973. module.exports.baseTag = baseTag;
  12974. module.exports.buildHeadTags = buildHeadTags;
  12975. module.exports.collectEmbeddedDiagnostics = collectEmbeddedDiagnostics;
  12976. module.exports.decodeEntities = decodeEntities;
  12977. module.exports.embeddedText = embeddedText;
  12978. module.exports.escapeAttribute = escapeAttribute;
  12979. module.exports.escapeText = escapeText;
  12980. module.exports.isAsciiWhitespace = isSpace;
  12981. module.exports.metaTag = metaTag;
  12982. // WHATWG "ASCII whitespace" (tab / LF / FF / CR / space) — the tokenizer's
  12983. // whitespace class, exported under the spec's name.
  12984. module.exports.parseCssUrls = parseCssUrls;
  12985. module.exports.parseHtml = parseHtml;
  12986. module.exports.parseMsapplicationTask = parseMsapplicationTask;
  12987. module.exports.parseSrc = parseSrc;
  12988. module.exports.parseSrcset = parseSrcset;
  12989. module.exports.pickTransforms = pickTransforms;
  12990. module.exports.printer = printer;
  12991. module.exports.tokenize = tokenize;