QueryPath.php 145 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543
  1. <?php
  2. /** @file
  3. * The Query Path package provides tools for manipulating a Document Object Model.
  4. * The two major DOMs are the XML DOM and the HTML DOM. Using Query Path, you can
  5. * build, parse, search, and modify DOM documents.
  6. *
  7. * To use Query Path, this is the only file you should need to import.
  8. *
  9. * Standard usage:
  10. * @code
  11. * <?php
  12. * require 'QueryPath/QueryPath.php';
  13. * $qp = qp('#myID', '<?xml version="1.0"?><test><foo id="myID"/></test>');
  14. * $qp->append('<new><elements/></new>')->writeHTML();
  15. * ?>
  16. * @endcode
  17. *
  18. * The above would print (formatted for readability):
  19. * @code
  20. * <?xml version="1.0"?>
  21. * <test>
  22. * <foo id="myID">
  23. * <new>
  24. * <element/>
  25. * </new>
  26. * </foo>
  27. * </test>
  28. * @endcode
  29. *
  30. * To learn about the functions available to a Query Path object,
  31. * see {@link QueryPath}. The {@link qp()} function is used to build
  32. * new QueryPath objects. The documentation for that function explains the
  33. * wealth of arguments that the function can take.
  34. *
  35. * Included with the source code for QueryPath is a complete set of unit tests
  36. * as well as some example files. Those are good resources for learning about
  37. * how to apply QueryPath's tools. The full API documentation can be generated
  38. * from these files using PHPDocumentor.
  39. *
  40. * If you are interested in building extensions for QueryParser, see the
  41. * {@link QueryPathExtender} class. There, you will find information on adding
  42. * your own tools to QueryPath.
  43. *
  44. * QueryPath also comes with a full CSS 3 selector parser implementation. If
  45. * you are interested in reusing that in other code, you will want to start
  46. * with {@link CssEventHandler.php}, which is the event interface for the parser.
  47. *
  48. * All of the code in QueryPath is licensed under either the LGPL or an MIT-like
  49. * license (you may choose which you prefer). All of the code is Copyright, 2009
  50. * by Matt Butcher.
  51. *
  52. * @author M Butcher <matt @aleph-null.tv>
  53. * @license http://opensource.org/licenses/lgpl-2.1.php The GNU Lesser GPL (LGPL) or an MIT-like license.
  54. * @see QueryPath
  55. * @see qp()
  56. * @see http://querypath.org The QueryPath home page.
  57. * @see http://api.querypath.org An online version of the API docs.
  58. * @see http://technosophos.com For how-tos and examples.
  59. * @copyright Copyright (c) 2009, Matt Butcher.
  60. * @version -UNSTABLE%
  61. *
  62. */
  63. /** @addtogroup querypath_core Core API
  64. * Core classes and functions for QueryPath.
  65. *
  66. * These are the classes, objects, and functions that developers who use QueryPath
  67. * are likely to use. The qp() and htmlqp() functions are the best place to start,
  68. * while most of the frequently used methods are part of the QueryPath object.
  69. */
  70. /** @addtogroup querypath_util Utilities
  71. * Utility classes for QueryPath.
  72. *
  73. * These classes add important, but less-often used features to QueryPath. Some of
  74. * these are used transparently (QueryPathIterator). Others you can use directly in your
  75. * code (QueryPathEntities).
  76. */
  77. /* * @namespace QueryPath
  78. * The core classes that compose QueryPath.
  79. *
  80. * The QueryPath classes contain the brunt of the QueryPath code. If you are
  81. * interested in working with just the CSS engine, you may want to look at CssEventHandler,
  82. * which can be used without the rest of QueryPath. If you are interested in looking
  83. * carefully at QueryPath's implementation details, then the QueryPath class is where you
  84. * should begin. If you are interested in writing extensions, than you may want to look at
  85. * QueryPathExtension, and also at some of the simple extensions, such as QPXML.
  86. */
  87. /**
  88. * Regular expression for checking whether a string looks like XML.
  89. * @deprecated This is no longer used in QueryPath.
  90. */
  91. define('ML_EXP','/^[^<]*(<(.|\s)+>)[^>]*$/');
  92. /**
  93. * The CssEventHandler interfaces with the CSS parser.
  94. */
  95. require_once 'CssEventHandler.php';
  96. /**
  97. * The extender is used to provide support for extensions.
  98. */
  99. require_once 'QueryPathExtension.php';
  100. /**
  101. * Build a new Query Path.
  102. * This builds a new Query Path object. The new object can be used for
  103. * reading, search, and modifying a document.
  104. *
  105. * While it is permissible to directly create new instances of a QueryPath
  106. * implementation, it is not advised. Instead, you should use this function
  107. * as a factory.
  108. *
  109. * Example:
  110. * @code
  111. * <?php
  112. * qp(); // New empty QueryPath
  113. * qp('path/to/file.xml'); // From a file
  114. * qp('<html><head></head><body></body></html>'); // From HTML or XML
  115. * qp(QueryPath::XHTML_STUB); // From a basic HTML document.
  116. * qp(QueryPath::XHTML_STUB, 'title'); // Create one from a basic HTML doc and position it at the title element.
  117. *
  118. * // Most of the time, methods are chained directly off of this call.
  119. * qp(QueryPath::XHTML_STUB, 'body')->append('<h1>Title</h1>')->addClass('body-class');
  120. * ?>
  121. * @endcode
  122. *
  123. * This function is used internally by QueryPath. Anything that modifies the
  124. * behavior of this function may also modify the behavior of common QueryPath
  125. * methods.
  126. *
  127. * <b>Types of documents that QueryPath can support</b>
  128. *
  129. * qp() can take any of these as its first argument:
  130. *
  131. * - A string of XML or HTML (See {@link XHTML_STUB})
  132. * - A path on the file system or a URL
  133. * - A {@link DOMDocument} object
  134. * - A {@link SimpleXMLElement} object.
  135. * - A {@link DOMNode} object.
  136. * - An array of {@link DOMNode} objects (generally {@link DOMElement} nodes).
  137. * - Another {@link QueryPath} object.
  138. *
  139. * Keep in mind that most features of QueryPath operate on elements. Other
  140. * sorts of DOMNodes might not work with all features.
  141. *
  142. * <b>Supported Options</b>
  143. * - context: A stream context object. This is used to pass context info
  144. * to the underlying file IO subsystem.
  145. * - encoding: A valid character encoding, such as 'utf-8' or 'ISO-8859-1'.
  146. * The default is system-dependant, typically UTF-8. Note that this is
  147. * only used when creating new documents, not when reading existing content.
  148. * (See convert_to_encoding below.)
  149. * - parser_flags: An OR-combined set of parser flags. The flags supported
  150. * by the DOMDocument PHP class are all supported here.
  151. * - omit_xml_declaration: Boolean. If this is TRUE, then certain output
  152. * methods (like {@link QueryPath::xml()}) will omit the XML declaration
  153. * from the beginning of a document.
  154. * - replace_entities: Boolean. If this is TRUE, then any of the insertion
  155. * functions (before(), append(), etc.) will replace named entities with
  156. * their decimal equivalent, and will replace un-escaped ampersands with
  157. * a numeric entity equivalent.
  158. * - ignore_parser_warnings: Boolean. If this is TRUE, then E_WARNING messages
  159. * generated by the XML parser will not cause QueryPath to throw an exception.
  160. * This is useful when parsing
  161. * badly mangled HTML, or when failure to find files should not result in
  162. * an exception. By default, this is FALSE -- that is, parsing warnings and
  163. * IO warnings throw exceptions.
  164. * - convert_to_encoding: Use the MB library to convert the document to the
  165. * named encoding before parsing. This is useful for old HTML (set it to
  166. * iso-8859-1 for best results). If this is not supplied, no character set
  167. * conversion will be performed. See {@link mb_convert_encoding()}.
  168. * (QueryPath 1.3 and later)
  169. * - convert_from_encoding: If 'convert_to_encoding' is set, this option can be
  170. * used to explicitly define what character set the source document is using.
  171. * By default, QueryPath will allow the MB library to guess the encoding.
  172. * (QueryPath 1.3 and later)
  173. * - strip_low_ascii: If this is set to TRUE then markup will have all low ASCII
  174. * characters (<32) stripped out before parsing. This is good in cases where
  175. * icky HTML has (illegal) low characters in the document.
  176. * - use_parser: If 'xml', Parse the document as XML. If 'html', parse the
  177. * document as HTML. Note that the XML parser is very strict, while the
  178. * HTML parser is more lenient, but does enforce some of the DTD/Schema.
  179. * <i>By default, QueryPath autodetects the type.</i>
  180. * - escape_xhtml_js_css_sections: XHTML needs script and css sections to be
  181. * escaped. Yet older readers do not handle CDATA sections, and comments do not
  182. * work properly (for numerous reasons). By default, QueryPath's *XHTML methods
  183. * will wrap a script body with a CDATA declaration inside of C-style comments.
  184. * If you want to change this, you can set this option with one of the
  185. * JS_CSS_ESCAPE_* constants, or you can write your own.
  186. * - QueryPath_class: (ADVANCED) Use this to set the actual classname that
  187. * {@link qp()} loads as a QueryPath instance. It is assumed that the
  188. * class is either {@link QueryPath} or a subclass thereof. See the test
  189. * cases for an example.
  190. *
  191. * @ingroup querypath_core
  192. * @param mixed $document
  193. * A document in one of the forms listed above.
  194. * @param string $string
  195. * A CSS 3 selector.
  196. * @param array $options
  197. * An associative array of options. Currently supported options are listed above.
  198. * @return QueryPath
  199. */
  200. function qp($document = NULL, $string = NULL, $options = array()) {
  201. $qpClass = isset($options['QueryPath_class']) ? $options['QueryPath_class'] : 'QueryPath';
  202. $qp = new $qpClass($document, $string, $options);
  203. return $qp;
  204. }
  205. /**
  206. * A special-purpose version of {@link qp()} designed specifically for HTML.
  207. *
  208. * XHTML (if valid) can be easily parsed by {@link qp()} with no problems. However,
  209. * because of the way that libxml handles HTML, there are several common steps that
  210. * need to be taken to reliably parse non-XML HTML documents. This function is
  211. * a convenience tool for configuring QueryPath to parse HTML.
  212. *
  213. * The following options are automatically set unless overridden:
  214. * - ignore_parser_warnings: TRUE
  215. * - convert_to_encoding: ISO-8859-1 (the best for the HTML parser).
  216. * - convert_from_encoding: auto (autodetect encoding)
  217. * - use_parser: html
  218. *
  219. * Parser warning messages are also suppressed, so if the parser emits a warning,
  220. * the application will not be notified. This is equivalent to
  221. * calling @code@qp()@endcode.
  222. *
  223. * Warning: Character set conversions will only work if the Multi-Byte (mb) library
  224. * is installed and enabled. This is usually enabled, but not always.
  225. *
  226. * @ingroup querypath_core
  227. * @see qp()
  228. */
  229. function htmlqp($document = NULL, $selector = NULL, $options = array()) {
  230. // Need a way to force an HTML parse instead of an XML parse when the
  231. // doctype is XHTML, since many XHTML documents are not valid XML
  232. // (because of coding errors, not by design).
  233. $options += array(
  234. 'ignore_parser_warnings' => TRUE,
  235. 'convert_to_encoding' => 'ISO-8859-1',
  236. 'convert_from_encoding' => 'auto',
  237. //'replace_entities' => TRUE,
  238. 'use_parser' => 'html',
  239. // This is stripping actually necessary low ASCII.
  240. //'strip_low_ascii' => TRUE,
  241. );
  242. return @qp($document, $selector, $options);
  243. }
  244. /**
  245. * The Query Path object is the primary tool in this library.
  246. *
  247. * To create a new Query Path, use the {@link qp()} function.
  248. *
  249. * If you are new to these documents, start at the {@link QueryPath.php} page.
  250. * There you will find a quick guide to the tools contained in this project.
  251. *
  252. * A note on serialization: QueryPath uses DOM classes internally, and those
  253. * do not serialize well at all. In addition, QueryPath may contain many
  254. * extensions, and there is no guarantee that extensions can serialize. The
  255. * moral of the story: Don't serialize QueryPath.
  256. *
  257. * @see qp()
  258. * @see QueryPath.php
  259. * @ingroup querypath_core
  260. */
  261. class QueryPath implements IteratorAggregate, Countable {
  262. /**
  263. * The version string for this version of QueryPath.
  264. *
  265. * Standard releases will be of the following form: <MAJOR>.<MINOR>[.<PATCH>][-STABILITY].
  266. *
  267. * Examples:
  268. * - 2.0
  269. * - 2.1.1
  270. * - 2.0-alpha1
  271. *
  272. * Developer releases will always be of the form dev-<DATE>.
  273. *
  274. * @since 2.0
  275. */
  276. const VERSION = '-UNSTABLE%';
  277. /**
  278. * This is a stub HTML 4.01 document.
  279. *
  280. * <b>Using {@link QueryPath::XHTML_STUB} is preferred.</b>
  281. *
  282. * This is primarily for generating legacy HTML content. Modern web applications
  283. * should use {@link QueryPath::XHTML_STUB}.
  284. *
  285. * Use this stub with the HTML familiy of methods ({@link html()},
  286. * {@link writeHTML()}, {@link innerHTML()}).
  287. */
  288. const HTML_STUB = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
  289. <html lang="en">
  290. <head>
  291. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  292. <title>Untitled</title>
  293. </head>
  294. <body></body>
  295. </html>';
  296. /**
  297. * This is a stub XHTML document.
  298. *
  299. * Since XHTML is an XML format, you should use XML functions with this document
  300. * fragment. For example, you should use {@link xml()}, {@link innerXML()}, and
  301. * {@link writeXML()}.
  302. *
  303. * This can be passed into {@link qp()} to begin a new basic HTML document.
  304. *
  305. * Example:
  306. * @code
  307. * $qp = qp(QueryPath::XHTML_STUB); // Creates a new XHTML document
  308. * $qp->writeXML(); // Writes the document as well-formed XHTML.
  309. * @endcode
  310. * @since 2.0
  311. */
  312. const XHTML_STUB = '<?xml version="1.0"?>
  313. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  314. <html xmlns="http://www.w3.org/1999/xhtml">
  315. <head>
  316. <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  317. <title>Untitled</title>
  318. </head>
  319. <body></body>
  320. </html>';
  321. /**
  322. * Default parser flags.
  323. *
  324. * These are flags that will be used if no global or local flags override them.
  325. * @since 2.0
  326. */
  327. const DEFAULT_PARSER_FLAGS = NULL;
  328. const JS_CSS_ESCAPE_CDATA = '\\1';
  329. const JS_CSS_ESCAPE_CDATA_CCOMMENT = '/* \\1 */';
  330. const JS_CSS_ESCAPE_CDATA_DOUBLESLASH = '// \\1';
  331. const JS_CSS_ESCAPE_NONE = '';
  332. //const IGNORE_ERRORS = 1544; //E_NOTICE | E_USER_WARNING | E_USER_NOTICE;
  333. private $errTypes = 771; //E_ERROR; | E_USER_ERROR;
  334. /**
  335. * The base DOMDocument.
  336. */
  337. protected $document = NULL;
  338. private $options = array(
  339. 'parser_flags' => NULL,
  340. 'omit_xml_declaration' => FALSE,
  341. 'replace_entities' => FALSE,
  342. 'exception_level' => 771, // E_ERROR | E_USER_ERROR | E_USER_WARNING | E_WARNING
  343. 'ignore_parser_warnings' => FALSE,
  344. 'escape_xhtml_js_css_sections' => self::JS_CSS_ESCAPE_CDATA_CCOMMENT,
  345. );
  346. /**
  347. * The array of matches.
  348. */
  349. protected $matches = array();
  350. /**
  351. * The last array of matches.
  352. */
  353. protected $last = array(); // Last set of matches.
  354. private $ext = array(); // Extensions array.
  355. /**
  356. * The number of current matches.
  357. *
  358. * @see count()
  359. */
  360. public $length = 0;
  361. /**
  362. * Constructor.
  363. *
  364. * This should not be called directly. Use the {@link qp()} factory function
  365. * instead.
  366. *
  367. * @param mixed $document
  368. * A document-like object.
  369. * @param string $string
  370. * A CSS 3 Selector
  371. * @param array $options
  372. * An associative array of options.
  373. * @see qp()
  374. */
  375. public function __construct($document = NULL, $string = NULL, $options = array()) {
  376. $string = trim($string);
  377. $this->options = $options + QueryPathOptions::get() + $this->options;
  378. $parser_flags = isset($options['parser_flags']) ? $options['parser_flags'] : self::DEFAULT_PARSER_FLAGS;
  379. if (!empty($this->options['ignore_parser_warnings'])) {
  380. // Don't convert parser warnings into exceptions.
  381. $this->errTypes = 257; //E_ERROR | E_USER_ERROR;
  382. }
  383. elseif (isset($this->options['exception_level'])) {
  384. // Set the error level at which exceptions will be thrown. By default,
  385. // QueryPath will throw exceptions for
  386. // E_ERROR | E_USER_ERROR | E_WARNING | E_USER_WARNING.
  387. $this->errTypes = $this->options['exception_level'];
  388. }
  389. // Empty: Just create an empty QP.
  390. if (empty($document)) {
  391. $this->document = isset($this->options['encoding']) ? new DOMDocument('1.0', $this->options['encoding']) : new DOMDocument();
  392. $this->setMatches(new SplObjectStorage());
  393. }
  394. // Figure out if document is DOM, HTML/XML, or a filename
  395. elseif (is_object($document)) {
  396. if ($document instanceof QueryPath) {
  397. $this->matches = $document->get(NULL, TRUE);
  398. if ($this->matches->count() > 0)
  399. $this->document = $this->getFirstMatch()->ownerDocument;
  400. }
  401. elseif ($document instanceof DOMDocument) {
  402. $this->document = $document;
  403. //$this->matches = $this->matches($document->documentElement);
  404. $this->setMatches($document->documentElement);
  405. }
  406. elseif ($document instanceof DOMNode) {
  407. $this->document = $document->ownerDocument;
  408. //$this->matches = array($document);
  409. $this->setMatches($document);
  410. }
  411. elseif ($document instanceof SimpleXMLElement) {
  412. $import = dom_import_simplexml($document);
  413. $this->document = $import->ownerDocument;
  414. //$this->matches = array($import);
  415. $this->setMatches($import);
  416. }
  417. elseif ($document instanceof SplObjectStorage) {
  418. if ($document->count() == 0) {
  419. throw new QueryPathException('Cannot initialize QueryPath from an empty SplObjectStore');
  420. }
  421. $this->matches = $document;
  422. $this->document = $this->getFirstMatch()->ownerDocument;
  423. }
  424. else {
  425. throw new QueryPathException('Unsupported class type: ' . get_class($document));
  426. }
  427. }
  428. elseif (is_array($document)) {
  429. //trigger_error('Detected deprecated array support', E_USER_NOTICE);
  430. if (!empty($document) && $document[0] instanceof DOMNode) {
  431. $found = new SplObjectStorage();
  432. foreach ($document as $item) $found->attach($item);
  433. //$this->matches = $found;
  434. $this->setMatches($found);
  435. $this->document = $this->getFirstMatch()->ownerDocument;
  436. }
  437. }
  438. elseif ($this->isXMLish($document)) {
  439. // $document is a string with XML
  440. $this->document = $this->parseXMLString($document);
  441. $this->setMatches($this->document->documentElement);
  442. }
  443. else {
  444. // $document is a filename
  445. $context = empty($options['context']) ? NULL : $options['context'];
  446. $this->document = $this->parseXMLFile($document, $parser_flags, $context);
  447. $this->setMatches($this->document->documentElement);
  448. }
  449. // Do a find if the second param was set.
  450. if (isset($string) && strlen($string) > 0) {
  451. $this->find($string);
  452. }
  453. }
  454. /**
  455. * A static function for transforming data into a Data URL.
  456. *
  457. * This can be used to create Data URLs for injection into CSS, JavaScript, or other
  458. * non-XML/HTML content. If you are working with QP objects, you may want to use
  459. * {@link dataURL()} instead.
  460. *
  461. * @param mixed $data
  462. * The contents to inject as the data. The value can be any one of the following:
  463. * - A URL: If this is given, then the subsystem will read the content from that URL. THIS
  464. * MUST BE A FULL URL, not a relative path.
  465. * - A string of data: If this is given, then the subsystem will encode the string.
  466. * - A stream or file handle: If this is given, the stream's contents will be encoded
  467. * and inserted as data.
  468. * (Note that we make the assumption here that you would never want to set data to be
  469. * a URL. If this is an incorrect assumption, file a bug.)
  470. * @param string $mime
  471. * The MIME type of the document.
  472. * @param resource $context
  473. * A valid context. Use this only if you need to pass a stream context. This is only necessary
  474. * if $data is a URL. (See {@link stream_context_create()}).
  475. * @return
  476. * An encoded data URL.
  477. */
  478. public static function encodeDataURL($data, $mime = 'application/octet-stream', $context = NULL) {
  479. if (is_resource($data)) {
  480. $data = stream_get_contents($data);
  481. }
  482. elseif (filter_var($data, FILTER_VALIDATE_URL)) {
  483. $data = file_get_contents($data, FALSE, $context);
  484. }
  485. $encoded = base64_encode($data);
  486. return 'data:' . $mime . ';base64,' . $encoded;
  487. }
  488. /**
  489. * Get the effective options for the current QueryPath object.
  490. *
  491. * This returns an associative array of all of the options as set
  492. * for the current QueryPath object. This includes default options,
  493. * options directly passed in via {@link qp()} or the constructor,
  494. * an options set in the {@link QueryPathOptions} object.
  495. *
  496. * The order of merging options is this:
  497. * - Options passed in using {@link qp()} are highest priority, and will
  498. * override other options.
  499. * - Options set with {@link QueryPathOptions} will override default options,
  500. * but can be overridden by options passed into {@link qp()}.
  501. * - Default options will be used when no overrides are present.
  502. *
  503. * This function will return the options currently used, with the above option
  504. * overriding having been calculated already.
  505. *
  506. * @return array
  507. * An associative array of options, calculated from defaults and overridden
  508. * options.
  509. * @see qp()
  510. * @see QueryPathOptions::set()
  511. * @see QueryPathOptions::merge()
  512. * @since 2.0
  513. */
  514. public function getOptions() {
  515. return $this->options;
  516. }
  517. /**
  518. * Select the root element of the document.
  519. *
  520. * This sets the current match to the document's root element. For
  521. * practical purposes, this is the same as:
  522. * @code
  523. * qp($someDoc)->find(':root');
  524. * @endcode
  525. * However, since it doesn't invoke a parser, it has less overhead. It also
  526. * works in cases where the QueryPath has been reduced to zero elements (a
  527. * case that is not handled by find(':root') because there is no element
  528. * whose root can be found).
  529. *
  530. * @param string $selector
  531. * A selector. If this is supplied, QueryPath will navigate to the
  532. * document root and then run the query. (Added in QueryPath 2.0 Beta 2)
  533. * @return QueryPath
  534. * The QueryPath object, wrapping the root element (document element)
  535. * for the current document.
  536. */
  537. public function top($selector = NULL) {
  538. $this->setMatches($this->document->documentElement);
  539. // print '=====================' . PHP_EOL;
  540. // var_dump($this->document);
  541. // print '=====================' . PHP_EOL;
  542. return !empty($selector) ? $this->find($selector) : $this;
  543. }
  544. /**
  545. * Given a CSS Selector, find matching items.
  546. *
  547. * @param string $selector
  548. * CSS 3 Selector
  549. * @return QueryPath
  550. * @see filter()
  551. * @see is()
  552. * @todo If a find() returns zero matches, then a subsequent find() will
  553. * also return zero matches, even if that find has a selector like :root.
  554. * The reason for this is that the {@link QueryPathCssEventHandler} does
  555. * not set the root of the document tree if it cannot find any elements
  556. * from which to determine what the root is. The workaround is to use
  557. * {@link top()} to select the root element again.
  558. */
  559. public function find($selector) {
  560. // Optimize for ID/Class searches. These two take a long time
  561. // when a rdp is used. Using an XPath pushes work to C code.
  562. $ids = array();
  563. $regex = '/^#([\w-]+)$|^\.([\w-]+)$/'; // $1 is ID, $2 is class.
  564. //$regex = '/^#([\w-]+)$/';
  565. if (preg_match($regex, $selector, $ids) === 1) {
  566. // If $1 is a match, we have an ID.
  567. if (!empty($ids[1])) {
  568. $xpath = new DOMXPath($this->document);
  569. foreach ($this->matches as $item) {
  570. // For whatever reasons, the .// does not work correctly
  571. // if the selected element is the root element. So we have
  572. // an awful hack.
  573. if ($item->isSameNode($this->document->documentElement) ) {
  574. $xpathQuery = "//*[@id='{$ids[1]}']";
  575. }
  576. // This is the correct XPath query.
  577. else {
  578. $xpathQuery = ".//*[@id='{$ids[1]}']";
  579. }
  580. //$nl = $xpath->query("//*[@id='{$ids[1]}']", $item);
  581. //$nl = $xpath->query(".//*[@id='{$ids[1]}']", $item);
  582. $nl = $xpath->query($xpathQuery, $item);
  583. if ($nl->length > 0) {
  584. $this->setMatches($nl->item(0));
  585. break;
  586. }
  587. else {
  588. // If no match is found, we set an empty.
  589. $this->noMatches();
  590. }
  591. }
  592. }
  593. // Quick search for class values. While the XPath can't do it
  594. // all, it is faster than doing a recusive node search.
  595. else {
  596. $xpath = new DOMXPath($this->document);
  597. $found = new SplObjectStorage();
  598. foreach ($this->matches as $item) {
  599. // See comments on this in the #id code above.
  600. if ($item->isSameNode($this->document->documentElement) ) {
  601. $xpathQuery = "//*[@class]";
  602. }
  603. // This is the correct XPath query.
  604. else {
  605. $xpathQuery = ".//*[@class]";
  606. }
  607. $nl = $xpath->query($xpathQuery, $item);
  608. for ($i = 0; $i < $nl->length; ++$i) {
  609. $vals = explode(' ', $nl->item($i)->getAttribute('class'));
  610. if (in_array($ids[2], $vals)) $found->attach($nl->item($i));
  611. }
  612. }
  613. $this->setMatches($found);
  614. }
  615. return $this;
  616. }
  617. $query = new QueryPathCssEventHandler($this->matches);
  618. $query->find($selector);
  619. //$this->matches = $query->getMatches();
  620. $this->setMatches($query->getMatches());
  621. return $this;
  622. }
  623. /**
  624. * Execute an XPath query and store the results in the QueryPath.
  625. *
  626. * Most methods in this class support CSS 3 Selectors. Sometimes, though,
  627. * XPath provides a finer-grained query language. Use this to execute
  628. * XPath queries.
  629. *
  630. * Beware, though. QueryPath works best on DOM Elements, but an XPath
  631. * query can return other nodes, strings, and values. These may not work with
  632. * other QueryPath functions (though you will be able to access the
  633. * values with {@link get()}).
  634. *
  635. * @param string $query
  636. * An XPath query.
  637. * @param array $options
  638. * Currently supported options are:
  639. * - 'namespace_prefix': And XML namespace prefix to be used as the default. Used
  640. * in conjunction with 'namespace_uri'
  641. * - 'namespace_uri': The URI to be used as the default namespace URI. Used
  642. * with 'namespace_prefix'
  643. * @return QueryPath
  644. * A QueryPath object wrapping the results of the query.
  645. * @see find()
  646. * @author M Butcher
  647. * @author Xavier Prud'homme
  648. */
  649. public function xpath($query, $options = array()) {
  650. $xpath = new DOMXPath($this->document);
  651. // Register a default namespace.
  652. if (!empty($options['namespace_prefix']) && !empty($options['namespace_uri'])) {
  653. $xpath->registerNamespace($options['namespace_prefix'], $options['namespace_uri']);
  654. }
  655. $found = new SplObjectStorage();
  656. foreach ($this->matches as $item) {
  657. $nl = $xpath->query($query, $item);
  658. if ($nl->length > 0) {
  659. for ($i = 0; $i < $nl->length; ++$i) $found->attach($nl->item($i));
  660. }
  661. }
  662. $this->setMatches($found);
  663. return $this;
  664. }
  665. /**
  666. * Get the number of elements currently wrapped by this object.
  667. *
  668. * Note that there is no length property on this object.
  669. *
  670. * @return int
  671. * Number of items in the object.
  672. * @deprecated QueryPath now implements Countable, so use count().
  673. */
  674. public function size() {
  675. return $this->matches->count();
  676. }
  677. /**
  678. * Get the number of elements currently wrapped by this object.
  679. *
  680. * Since QueryPath is Countable, the PHP count() function can also
  681. * be used on a QueryPath.
  682. *
  683. * @code
  684. * <?php
  685. * count(qp($xml, 'div'));
  686. * ?>
  687. * @endcode
  688. *
  689. * @return int
  690. * The number of matches in the QueryPath.
  691. */
  692. public function count() {
  693. return $this->matches->count();
  694. }
  695. /**
  696. * Get one or all elements from this object.
  697. *
  698. * When called with no paramaters, this returns all objects wrapped by
  699. * the QueryPath. Typically, these are DOMElement objects (unless you have
  700. * used {@link map()}, {@link xpath()}, or other methods that can select
  701. * non-elements).
  702. *
  703. * When called with an index, it will return the item in the QueryPath with
  704. * that index number.
  705. *
  706. * Calling this method does not change the QueryPath (e.g. it is
  707. * non-destructive).
  708. *
  709. * You can use qp()->get() to iterate over all elements matched. You can
  710. * also iterate over qp() itself (QueryPath implementations must be Traversable).
  711. * In the later case, though, each item
  712. * will be wrapped in a QueryPath object. To learn more about iterating
  713. * in QueryPath, see {@link examples/techniques.php}.
  714. *
  715. * @param int $index
  716. * If specified, then only this index value will be returned. If this
  717. * index is out of bounds, a NULL will be returned.
  718. * @param boolean $asObject
  719. * If this is TRUE, an {@link SplObjectStorage} object will be returned
  720. * instead of an array. This is the preferred method for extensions to use.
  721. * @return mixed
  722. * If an index is passed, one element will be returned. If no index is
  723. * present, an array of all matches will be returned.
  724. * @see eq()
  725. * @see SplObjectStorage
  726. */
  727. public function get($index = NULL, $asObject = FALSE) {
  728. if (isset($index)) {
  729. return ($this->size() > $index) ? $this->getNthMatch($index) : NULL;
  730. }
  731. // Retain support for legacy.
  732. if (!$asObject) {
  733. $matches = array();
  734. foreach ($this->matches as $m) $matches[] = $m;
  735. return $matches;
  736. }
  737. return $this->matches;
  738. }
  739. /**
  740. * Get the DOMDocument that we currently work with.
  741. *
  742. * This returns the current DOMDocument. Any changes made to this document will be
  743. * accessible to QueryPath, as both will share access to the same object.
  744. *
  745. * @return DOMDocument
  746. */
  747. public function document() {
  748. return $this->document;
  749. }
  750. /**
  751. * On an XML document, load all XIncludes.
  752. *
  753. * @return QueryPath
  754. */
  755. public function xinclude() {
  756. $this->document->xinclude();
  757. return $this;
  758. }
  759. /**
  760. * Get all current elements wrapped in an array.
  761. * Compatibility function for jQuery 1.4, but identical to calling {@link get()}
  762. * with no parameters.
  763. *
  764. * @return array
  765. * An array of DOMNodes (typically DOMElements).
  766. */
  767. public function toArray() {
  768. return $this->get();
  769. }
  770. /**
  771. * Get/set an attribute.
  772. * - If no parameters are specified, this returns an associative array of all
  773. * name/value pairs.
  774. * - If both $name and $value are set, then this will set the attribute name/value
  775. * pair for all items in this object.
  776. * - If $name is set, and is an array, then
  777. * all attributes in the array will be set for all items in this object.
  778. * - If $name is a string and is set, then the attribute value will be returned.
  779. *
  780. * When an attribute value is retrieved, only the attribute value of the FIRST
  781. * match is returned.
  782. *
  783. * @param mixed $name
  784. * The name of the attribute or an associative array of name/value pairs.
  785. * @param string $value
  786. * A value (used only when setting an individual property).
  787. * @return mixed
  788. * If this was a setter request, return the QueryPath object. If this was
  789. * an access request (getter), return the string value.
  790. * @see removeAttr()
  791. * @see tag()
  792. * @see hasAttr()
  793. * @see hasClass()
  794. */
  795. public function attr($name = NULL, $value = NULL) {
  796. // Default case: Return all attributes as an assoc array.
  797. if (is_null($name)) {
  798. if ($this->matches->count() == 0) return NULL;
  799. $ele = $this->getFirstMatch();
  800. $buffer = array();
  801. // This does not appear to be part of the DOM
  802. // spec. Nor is it documented. But it works.
  803. foreach ($ele->attributes as $name => $attrNode) {
  804. $buffer[$name] = $attrNode->value;
  805. }
  806. return $buffer;
  807. }
  808. // multi-setter
  809. if (is_array($name)) {
  810. foreach ($name as $k => $v) {
  811. foreach ($this->matches as $m) $m->setAttribute($k, $v);
  812. }
  813. return $this;
  814. }
  815. // setter
  816. if (isset($value)) {
  817. foreach ($this->matches as $m) $m->setAttribute($name, $value);
  818. return $this;
  819. }
  820. //getter
  821. if ($this->matches->count() == 0) return NULL;
  822. // Special node type handler:
  823. if ($name == 'nodeType') {
  824. return $this->getFirstMatch()->nodeType;
  825. }
  826. // Always return first match's attr.
  827. return $this->getFirstMatch()->getAttribute($name);
  828. }
  829. /**
  830. * Check to see if the given attribute is present.
  831. *
  832. * This returns TRUE if <em>all</em> selected items have the attribute, or
  833. * FALSE if at least one item does not have the attribute.
  834. *
  835. * @param string $attrName
  836. * The attribute name.
  837. * @return boolean
  838. * TRUE if all matches have the attribute, FALSE otherwise.
  839. * @since 2.0
  840. * @see attr()
  841. * @see hasClass()
  842. */
  843. public function hasAttr($attrName) {
  844. foreach ($this->matches as $match) {
  845. if (!$match->hasAttribute($attrName)) return FALSE;
  846. }
  847. return TRUE;
  848. }
  849. /**
  850. * Set/get a CSS value for the current element(s).
  851. * This sets the CSS value for each element in the QueryPath object.
  852. * It does this by setting (or getting) the style attribute (without a namespace).
  853. *
  854. * For example, consider this code:
  855. * @code
  856. * <?php
  857. * qp(HTML_STUB, 'body')->css('background-color','red')->html();
  858. * ?>
  859. * @endcode
  860. * This will return the following HTML:
  861. * @code
  862. * <body style="background-color: red"/>
  863. * @endcode
  864. *
  865. * If no parameters are passed into this function, then the current style
  866. * element will be returned unparsed. Example:
  867. * @code
  868. * <?php
  869. * qp(HTML_STUB, 'body')->css('background-color','red')->css();
  870. * ?>
  871. * @endcode
  872. * This will return the following:
  873. * @code
  874. * background-color: red
  875. * @endcode
  876. *
  877. * As of QueryPath 2.1, existing style attributes will be merged with new attributes.
  878. * (In previous versions of QueryPath, a call to css() overwrite the existing style
  879. * values).
  880. *
  881. * @param mixed $name
  882. * If this is a string, it will be used as a CSS name. If it is an array,
  883. * this will assume it is an array of name/value pairs of CSS rules. It will
  884. * apply all rules to all elements in the set.
  885. * @param string $value
  886. * The value to set. This is only set if $name is a string.
  887. * @return QueryPath
  888. */
  889. public function css($name = NULL, $value = '') {
  890. if (empty($name)) {
  891. return $this->attr('style');
  892. }
  893. // Get any existing CSS.
  894. $css = array();
  895. foreach ($this->matches as $match) {
  896. $style = $match->getAttribute('style');
  897. if (!empty($style)) {
  898. // XXX: Is this sufficient?
  899. $style_array = explode(';', $style);
  900. foreach ($style_array as $item) {
  901. $item = trim($item);
  902. // Skip empty attributes.
  903. if (strlen($item) == 0) continue;
  904. list($css_att, $css_val) = explode(':',$item, 2);
  905. $css[$css_att] = trim($css_val);
  906. }
  907. }
  908. }
  909. if (is_array($name)) {
  910. // Use array_merge instead of + to preserve order.
  911. $css = array_merge($css, $name);
  912. }
  913. else {
  914. $css[$name] = $value;
  915. }
  916. // Collapse CSS into a string.
  917. $format = '%s: %s;';
  918. $css_string = '';
  919. foreach ($css as $n => $v) {
  920. $css_string .= sprintf($format, $n, trim($v));
  921. }
  922. $this->attr('style', $css_string);
  923. return $this;
  924. }
  925. /**
  926. * Insert or retrieve a Data URL.
  927. *
  928. * When called with just $attr, it will fetch the result, attempt to decode it, and
  929. * return an array with the MIME type and the application data.
  930. *
  931. * When called with both $attr and $data, it will inject the data into all selected elements
  932. * So @code$qp->dataURL('src', file_get_contents('my.png'), 'image/png')@endcode will inject
  933. * the given PNG image into the selected elements.
  934. *
  935. * The current implementation only knows how to encode and decode Base 64 data.
  936. *
  937. * Note that this is known *not* to work on IE 6, but should render fine in other browsers.
  938. *
  939. * @param string $attr
  940. * The name of the attribute.
  941. * @param mixed $data
  942. * The contents to inject as the data. The value can be any one of the following:
  943. * - A URL: If this is given, then the subsystem will read the content from that URL. THIS
  944. * MUST BE A FULL URL, not a relative path.
  945. * - A string of data: If this is given, then the subsystem will encode the string.
  946. * - A stream or file handle: If this is given, the stream's contents will be encoded
  947. * and inserted as data.
  948. * (Note that we make the assumption here that you would never want to set data to be
  949. * a URL. If this is an incorrect assumption, file a bug.)
  950. * @param string $mime
  951. * The MIME type of the document.
  952. * @param resource $context
  953. * A valid context. Use this only if you need to pass a stream context. This is only necessary
  954. * if $data is a URL. (See {@link stream_context_create()}).
  955. * @return
  956. * If this is called as a setter, this will return a QueryPath object. Otherwise, it
  957. * will attempt to fetch data out of the attribute and return that.
  958. * @see http://en.wikipedia.org/wiki/Data:_URL
  959. * @see attr()
  960. * @since 2.1
  961. */
  962. public function dataURL($attr, $data = NULL, $mime = 'application/octet-stream', $context = NULL) {
  963. if (is_null($data)) {
  964. // Attempt to fetch the data
  965. $data = $this->attr($attr);
  966. if (empty($data) || is_array($data) || strpos($data, 'data:') !== 0) {
  967. return;
  968. }
  969. // So 1 and 2 should be MIME types, and 3 should be the base64-encoded data.
  970. $regex = '/^data:([a-zA-Z0-9]+)\/([a-zA-Z0-9]+);base64,(.*)$/';
  971. $matches = array();
  972. preg_match($regex, $data, $matches);
  973. if (!empty($matches)) {
  974. $result = array(
  975. 'mime' => $matches[1] . '/' . $matches[2],
  976. 'data' => base64_decode($matches[3]),
  977. );
  978. return $result;
  979. }
  980. }
  981. else {
  982. $attVal = self::encodeDataURL($data, $mime, $context);
  983. return $this->attr($attr, $attVal);
  984. }
  985. }
  986. /**
  987. * Remove the named attribute from all elements in the current QueryPath.
  988. *
  989. * This will remove any attribute with the given name. It will do this on each
  990. * item currently wrapped by QueryPath.
  991. *
  992. * As is the case in jQuery, this operation is not considered destructive.
  993. *
  994. * @param string $name
  995. * Name of the parameter to remove.
  996. * @return QueryPath
  997. * The QueryPath object with the same elements.
  998. * @see attr()
  999. */
  1000. public function removeAttr($name) {
  1001. foreach ($this->matches as $m) {
  1002. //if ($m->hasAttribute($name))
  1003. $m->removeAttribute($name);
  1004. }
  1005. return $this;
  1006. }
  1007. /**
  1008. * Reduce the matched set to just one.
  1009. *
  1010. * This will take a matched set and reduce it to just one item -- the item
  1011. * at the index specified. This is a destructive operation, and can be undone
  1012. * with {@link end()}.
  1013. *
  1014. * @param $index
  1015. * The index of the element to keep. The rest will be
  1016. * discarded.
  1017. * @return QueryPath
  1018. * @see get()
  1019. * @see is()
  1020. * @see end()
  1021. */
  1022. public function eq($index) {
  1023. // XXX: Might there be a more efficient way of doing this?
  1024. $this->setMatches($this->getNthMatch($index));
  1025. return $this;
  1026. }
  1027. /**
  1028. * Given a selector, this checks to see if the current set has one or more matches.
  1029. *
  1030. * Unlike jQuery's version, this supports full selectors (not just simple ones).
  1031. *
  1032. * @param string $selector
  1033. * The selector to search for. As of QueryPath 2.1.1, this also supports passing a
  1034. * DOMNode object.
  1035. * @return boolean
  1036. * TRUE if one or more elements match. FALSE if no match is found.
  1037. * @see get()
  1038. * @see eq()
  1039. */
  1040. public function is($selector) {
  1041. if (is_object($selector)) {
  1042. if ($selector instanceof DOMNode) {
  1043. return count($this->matches) == 1 && $selector->isSameNode($this->get(0));
  1044. }
  1045. elseif ($selector instanceof Traversable) {
  1046. if (count($selector) != count($this->matches)) {
  1047. return FALSE;
  1048. }
  1049. // Without $seen, there is an edge case here if $selector contains the same object
  1050. // more than once, but the counts are equal. For example, [a, a, a, a] will
  1051. // pass an is() on [a, b, c, d]. We use the $seen SPLOS to prevent this.
  1052. $seen = new SplObjectStorage();
  1053. foreach ($selector as $item) {
  1054. if (!$this->matches->contains($item) || $seen->contains($item)) {
  1055. return FALSE;
  1056. }
  1057. $seen->attach($item);
  1058. }
  1059. return TRUE;
  1060. }
  1061. throw new Exception('Cannot compare an object to a QueryPath.');
  1062. return FALSE;
  1063. }
  1064. foreach ($this->matches as $m) {
  1065. $q = new QueryPathCssEventHandler($m);
  1066. if ($q->find($selector)->getMatches()->count()) {
  1067. return TRUE;
  1068. }
  1069. }
  1070. return FALSE;
  1071. }
  1072. /**
  1073. * Filter a list down to only elements that match the selector.
  1074. * Use this, for example, to find all elements with a class, or with
  1075. * certain children.
  1076. *
  1077. * @param string $selector
  1078. * The selector to use as a filter.
  1079. * @return QueryPath
  1080. * The QueryPath with non-matching items filtered out.
  1081. * @see filterLambda()
  1082. * @see filterCallback()
  1083. * @see map()
  1084. * @see find()
  1085. * @see is()
  1086. */
  1087. public function filter($selector) {
  1088. $found = new SplObjectStorage();
  1089. foreach ($this->matches as $m) if (qp($m, NULL, $this->options)->is($selector)) $found->attach($m);
  1090. $this->setMatches($found);
  1091. return $this;
  1092. }
  1093. /**
  1094. * Filter based on a lambda function.
  1095. *
  1096. * The function string will be executed as if it were the body of a
  1097. * function. It is passed two arguments:
  1098. * - $index: The index of the item.
  1099. * - $item: The current Element.
  1100. * If the function returns boolean FALSE, the item will be removed from
  1101. * the list of elements. Otherwise it will be kept.
  1102. *
  1103. * Example:
  1104. * @code
  1105. * qp('li')->filterLambda('qp($item)->attr("id") == "test"');
  1106. * @endcode
  1107. *
  1108. * The above would filter down the list to only an item whose ID is
  1109. * 'text'.
  1110. *
  1111. * @param string $fn
  1112. * Inline lambda function in a string.
  1113. * @return QueryPath
  1114. * @see filter()
  1115. * @see map()
  1116. * @see mapLambda()
  1117. * @see filterCallback()
  1118. */
  1119. public function filterLambda($fn) {
  1120. $function = create_function('$index, $item', $fn);
  1121. $found = new SplObjectStorage();
  1122. $i = 0;
  1123. foreach ($this->matches as $item)
  1124. if ($function($i++, $item) !== FALSE) $found->attach($item);
  1125. $this->setMatches($found);
  1126. return $this;
  1127. }
  1128. /**
  1129. * Use regular expressions to filter based on the text content of matched elements.
  1130. *
  1131. * Only items that match the given regular expression will be kept. All others will
  1132. * be removed.
  1133. *
  1134. * The regular expression is run against the <i>text content</i> (the PCDATA) of the
  1135. * elements. This is a way of filtering elements based on their content.
  1136. *
  1137. * Example:
  1138. * @code
  1139. * <?xml version="1.0"?>
  1140. * <div>Hello <i>World</i></div>
  1141. * @endcode
  1142. *
  1143. * @code
  1144. * <?php
  1145. * // This will be 1.
  1146. * qp($xml, 'div')->filterPreg('/World/')->size();
  1147. * ?>
  1148. * @endcode
  1149. *
  1150. * The return value above will be 1 because the text content of @codeqp($xml, 'div')@endcode is
  1151. * @codeHello World@endcode.
  1152. *
  1153. * Compare this to the behavior of the <em>:contains()</em> CSS3 pseudo-class.
  1154. *
  1155. * @param string $regex
  1156. * A regular expression.
  1157. * @return QueryPath
  1158. * @see filter()
  1159. * @see filterCallback()
  1160. * @see preg_match()
  1161. */
  1162. public function filterPreg($regex) {
  1163. $found = new SplObjectStorage();
  1164. foreach ($this->matches as $item) {
  1165. if (preg_match($regex, $item->textContent) > 0) {
  1166. $found->attach($item);
  1167. }
  1168. }
  1169. $this->setMatches($found);
  1170. return $this;
  1171. }
  1172. /**
  1173. * Filter based on a callback function.
  1174. *
  1175. * A callback may be any of the following:
  1176. * - a function: 'my_func'.
  1177. * - an object/method combo: $obj, 'myMethod'
  1178. * - a class/method combo: 'MyClass', 'myMethod'
  1179. * Note that classes are passed in strings. Objects are not.
  1180. *
  1181. * Each callback is passed to arguments:
  1182. * - $index: The index position of the object in the array.
  1183. * - $item: The item to be operated upon.
  1184. *
  1185. * If the callback function returns FALSE, the item will be removed from the
  1186. * set of matches. Otherwise the item will be considered a match and left alone.
  1187. *
  1188. * @param callback $callback.
  1189. * A callback either as a string (function) or an array (object, method OR
  1190. * classname, method).
  1191. * @return QueryPath
  1192. * Query path object augmented according to the function.
  1193. * @see filter()
  1194. * @see filterLambda()
  1195. * @see map()
  1196. * @see is()
  1197. * @see find()
  1198. */
  1199. public function filterCallback($callback) {
  1200. $found = new SplObjectStorage();
  1201. $i = 0;
  1202. if (is_callable($callback)) {
  1203. foreach($this->matches as $item)
  1204. if (call_user_func($callback, $i++, $item) !== FALSE) $found->attach($item);
  1205. }
  1206. else {
  1207. throw new QueryPathException('The specified callback is not callable.');
  1208. }
  1209. $this->setMatches($found);
  1210. return $this;
  1211. }
  1212. /**
  1213. * Filter a list to contain only items that do NOT match.
  1214. *
  1215. * @param string $selector
  1216. * A selector to use as a negation filter. If the filter is matched, the
  1217. * element will be removed from the list.
  1218. * @return QueryPath
  1219. * The QueryPath object with matching items filtered out.
  1220. * @see find()
  1221. */
  1222. public function not($selector) {
  1223. $found = new SplObjectStorage();
  1224. if ($selector instanceof DOMElement) {
  1225. foreach ($this->matches as $m) if ($m !== $selector) $found->attach($m);
  1226. }
  1227. elseif (is_array($selector)) {
  1228. foreach ($this->matches as $m) {
  1229. if (!in_array($m, $selector, TRUE)) $found->attach($m);
  1230. }
  1231. }
  1232. elseif ($selector instanceof SplObjectStorage) {
  1233. foreach ($this->matches as $m) if ($selector->contains($m)) $found->attach($m);
  1234. }
  1235. else {
  1236. foreach ($this->matches as $m) if (!qp($m, NULL, $this->options)->is($selector)) $found->attach($m);
  1237. }
  1238. $this->setMatches($found);
  1239. return $this;
  1240. }
  1241. /**
  1242. * Get an item's index.
  1243. *
  1244. * Given a DOMElement, get the index from the matches. This is the
  1245. * converse of {@link get()}.
  1246. *
  1247. * @param DOMElement $subject
  1248. * The item to match.
  1249. *
  1250. * @return mixed
  1251. * The index as an integer (if found), or boolean FALSE. Since 0 is a
  1252. * valid index, you should use strong equality (===) to test..
  1253. * @see get()
  1254. * @see is()
  1255. */
  1256. public function index($subject) {
  1257. $i = 0;
  1258. foreach ($this->matches as $m) {
  1259. if ($m === $subject) {
  1260. return $i;
  1261. }
  1262. ++$i;
  1263. }
  1264. return FALSE;
  1265. }
  1266. /**
  1267. * Run a function on each item in a set.
  1268. *
  1269. * The mapping callback can return anything. Whatever it returns will be
  1270. * stored as a match in the set, though. This means that afer a map call,
  1271. * there is no guarantee that the elements in the set will behave correctly
  1272. * with other QueryPath functions.
  1273. *
  1274. * Callback rules:
  1275. * - If the callback returns NULL, the item will be removed from the array.
  1276. * - If the callback returns an array, the entire array will be stored in
  1277. * the results.
  1278. * - If the callback returns anything else, it will be appended to the array
  1279. * of matches.
  1280. *
  1281. * @param callback $callback
  1282. * The function or callback to use. The callback will be passed two params:
  1283. * - $index: The index position in the list of items wrapped by this object.
  1284. * - $item: The current item.
  1285. *
  1286. * @return QueryPath
  1287. * The QueryPath object wrapping a list of whatever values were returned
  1288. * by each run of the callback.
  1289. *
  1290. * @see QueryPath::get()
  1291. * @see filter()
  1292. * @see find()
  1293. */
  1294. public function map($callback) {
  1295. $found = new SplObjectStorage();
  1296. if (is_callable($callback)) {
  1297. $i = 0;
  1298. foreach ($this->matches as $item) {
  1299. $c = call_user_func($callback, $i, $item);
  1300. if (isset($c)) {
  1301. if (is_array($c) || $c instanceof Iterable) {
  1302. foreach ($c as $retval) {
  1303. if (!is_object($retval)) {
  1304. $tmp = new stdClass();
  1305. $tmp->textContent = $retval;
  1306. $retval = $tmp;
  1307. }
  1308. $found->attach($retval);
  1309. }
  1310. }
  1311. else {
  1312. if (!is_object($c)) {
  1313. $tmp = new stdClass();
  1314. $tmp->textContent = $c;
  1315. $c = $tmp;
  1316. }
  1317. $found->attach($c);
  1318. }
  1319. }
  1320. ++$i;
  1321. }
  1322. }
  1323. else {
  1324. throw new QueryPathException('Callback is not callable.');
  1325. }
  1326. $this->setMatches($found, FALSE);
  1327. return $this;
  1328. }
  1329. /**
  1330. * Narrow the items in this object down to only a slice of the starting items.
  1331. *
  1332. * @param integer $start
  1333. * Where in the list of matches to begin the slice.
  1334. * @param integer $length
  1335. * The number of items to include in the slice. If nothing is specified, the
  1336. * all remaining matches (from $start onward) will be included in the sliced
  1337. * list.
  1338. * @return QueryPath
  1339. * @see array_slice()
  1340. */
  1341. public function slice($start, $length = 0) {
  1342. $end = $length;
  1343. $found = new SplObjectStorage();
  1344. if ($start >= $this->size()) {
  1345. $this->setMatches($found);
  1346. return $this;
  1347. }
  1348. $i = $j = 0;
  1349. foreach ($this->matches as $m) {
  1350. if ($i >= $start) {
  1351. if ($end > 0 && $j >= $end) {
  1352. break;
  1353. }
  1354. $found->attach($m);
  1355. ++$j;
  1356. }
  1357. ++$i;
  1358. }
  1359. $this->setMatches($found);
  1360. return $this;
  1361. }
  1362. /**
  1363. * Run a callback on each item in the list of items.
  1364. *
  1365. * Rules of the callback:
  1366. * - A callback is passed two variables: $index and $item. (There is no
  1367. * special treatment of $this, as there is in jQuery.)
  1368. * - You will want to pass $item by reference if it is not an
  1369. * object (DOMNodes are all objects).
  1370. * - A callback that returns FALSE will stop execution of the each() loop. This
  1371. * works like break in a standard loop.
  1372. * - A TRUE return value from the callback is analogous to a continue statement.
  1373. * - All other return values are ignored.
  1374. *
  1375. * @param callback $callback
  1376. * The callback to run.
  1377. * @return QueryPath
  1378. * The QueryPath.
  1379. * @see eachLambda()
  1380. * @see filter()
  1381. * @see map()
  1382. */
  1383. public function each($callback) {
  1384. if (is_callable($callback)) {
  1385. $i = 0;
  1386. foreach ($this->matches as $item) {
  1387. if (call_user_func($callback, $i, $item) === FALSE) return $this;
  1388. ++$i;
  1389. }
  1390. }
  1391. else {
  1392. throw new QueryPathException('Callback is not callable.');
  1393. }
  1394. return $this;
  1395. }
  1396. /**
  1397. * An each() iterator that takes a lambda function.
  1398. *
  1399. * @param string $lambda
  1400. * The lambda function. This will be passed ($index, &$item).
  1401. * @return QueryPath
  1402. * The QueryPath object.
  1403. * @see each()
  1404. * @see filterLambda()
  1405. * @see filterCallback()
  1406. * @see map()
  1407. */
  1408. public function eachLambda($lambda) {
  1409. $index = 0;
  1410. foreach ($this->matches as $item) {
  1411. $fn = create_function('$index, &$item', $lambda);
  1412. if ($fn($index, $item) === FALSE) return $this;
  1413. ++$index;
  1414. }
  1415. return $this;
  1416. }
  1417. /**
  1418. * Insert the given markup as the last child.
  1419. *
  1420. * The markup will be inserted into each match in the set.
  1421. *
  1422. * The same element cannot be inserted multiple times into a document. DOM
  1423. * documents do not allow a single object to be inserted multiple times
  1424. * into the DOM. To insert the same XML repeatedly, we must first clone
  1425. * the object. This has one practical implication: Once you have inserted
  1426. * an element into the object, you cannot further manipulate the original
  1427. * element and expect the changes to be replciated in the appended object.
  1428. * (They are not the same -- there is no shared reference.) Instead, you
  1429. * will need to retrieve the appended object and operate on that.
  1430. *
  1431. * @param mixed $data
  1432. * This can be either a string (the usual case), or a DOM Element.
  1433. * @return QueryPath
  1434. * The QueryPath object.
  1435. * @see appendTo()
  1436. * @see prepend()
  1437. * @throws QueryPathException
  1438. * Thrown if $data is an unsupported object type.
  1439. */
  1440. public function append($data) {
  1441. $data = $this->prepareInsert($data);
  1442. if (isset($data)) {
  1443. if (empty($this->document->documentElement) && $this->matches->count() == 0) {
  1444. // Then we assume we are writing to the doc root
  1445. $this->document->appendChild($data);
  1446. $found = new SplObjectStorage();
  1447. $found->attach($this->document->documentElement);
  1448. $this->setMatches($found);
  1449. }
  1450. else {
  1451. // You can only append in item once. So in cases where we
  1452. // need to append multiple times, we have to clone the node.
  1453. foreach ($this->matches as $m) {
  1454. // DOMDocumentFragments are even more troublesome, as they don't
  1455. // always clone correctly. So we have to clone their children.
  1456. if ($data instanceof DOMDocumentFragment) {
  1457. foreach ($data->childNodes as $n)
  1458. $m->appendChild($n->cloneNode(TRUE));
  1459. }
  1460. else {
  1461. // Otherwise a standard clone will do.
  1462. $m->appendChild($data->cloneNode(TRUE));
  1463. }
  1464. }
  1465. }
  1466. }
  1467. return $this;
  1468. }
  1469. /**
  1470. * Append the current elements to the destination passed into the function.
  1471. *
  1472. * This cycles through all of the current matches and appends them to
  1473. * the context given in $destination. If a selector is provided then the
  1474. * $destination is queried (using that selector) prior to the data being
  1475. * appended. The data is then appended to the found items.
  1476. *
  1477. * @param QueryPath $dest
  1478. * A QueryPath object that will be appended to.
  1479. * @return QueryPath
  1480. * The original QueryPath, unaltered. Only the destination QueryPath will
  1481. * be modified.
  1482. * @see append()
  1483. * @see prependTo()
  1484. * @throws QueryPathException
  1485. * Thrown if $data is an unsupported object type.
  1486. */
  1487. public function appendTo(QueryPath $dest) {
  1488. foreach ($this->matches as $m) $dest->append($m);
  1489. return $this;
  1490. }
  1491. /**
  1492. * Insert the given markup as the first child.
  1493. *
  1494. * The markup will be inserted into each match in the set.
  1495. *
  1496. * @param mixed $data
  1497. * This can be either a string (the usual case), or a DOM Element.
  1498. * @return QueryPath
  1499. * @see append()
  1500. * @see before()
  1501. * @see after()
  1502. * @see prependTo()
  1503. * @throws QueryPathException
  1504. * Thrown if $data is an unsupported object type.
  1505. */
  1506. public function prepend($data) {
  1507. $data = $this->prepareInsert($data);
  1508. if (isset($data)) {
  1509. foreach ($this->matches as $m) {
  1510. $ins = $data->cloneNode(TRUE);
  1511. if ($m->hasChildNodes())
  1512. $m->insertBefore($ins, $m->childNodes->item(0));
  1513. else
  1514. $m->appendChild($ins);
  1515. }
  1516. }
  1517. return $this;
  1518. }
  1519. /**
  1520. * Take all nodes in the current object and prepend them to the children nodes of
  1521. * each matched node in the passed-in QueryPath object.
  1522. *
  1523. * This will iterate through each item in the current QueryPath object and
  1524. * add each item to the beginning of the children of each element in the
  1525. * passed-in QueryPath object.
  1526. *
  1527. * @see insertBefore()
  1528. * @see insertAfter()
  1529. * @see prepend()
  1530. * @see appendTo()
  1531. * @param QueryPath $dest
  1532. * The destination QueryPath object.
  1533. * @return QueryPath
  1534. * The original QueryPath, unmodified. NOT the destination QueryPath.
  1535. * @throws QueryPathException
  1536. * Thrown if $data is an unsupported object type.
  1537. */
  1538. public function prependTo(QueryPath $dest) {
  1539. foreach ($this->matches as $m) $dest->prepend($m);
  1540. return $this;
  1541. }
  1542. /**
  1543. * Insert the given data before each element in the current set of matches.
  1544. *
  1545. * This will take the give data (XML or HTML) and put it before each of the items that
  1546. * the QueryPath object currently contains. Contrast this with after().
  1547. *
  1548. * @param mixed $data
  1549. * The data to be inserted. This can be XML in a string, a DomFragment, a DOMElement,
  1550. * or the other usual suspects. (See {@link qp()}).
  1551. * @return QueryPath
  1552. * Returns the QueryPath with the new modifications. The list of elements currently
  1553. * selected will remain the same.
  1554. * @see insertBefore()
  1555. * @see after()
  1556. * @see append()
  1557. * @see prepend()
  1558. * @throws QueryPathException
  1559. * Thrown if $data is an unsupported object type.
  1560. */
  1561. public function before($data) {
  1562. $data = $this->prepareInsert($data);
  1563. foreach ($this->matches as $m) {
  1564. $ins = $data->cloneNode(TRUE);
  1565. $m->parentNode->insertBefore($ins, $m);
  1566. }
  1567. return $this;
  1568. }
  1569. /**
  1570. * Insert the current elements into the destination document.
  1571. * The items are inserted before each element in the given QueryPath document.
  1572. * That is, they will be siblings with the current elements.
  1573. *
  1574. * @param QueryPath $dest
  1575. * Destination QueryPath document.
  1576. * @return QueryPath
  1577. * The current QueryPath object, unaltered. Only the destination QueryPath
  1578. * object is altered.
  1579. * @see before()
  1580. * @see insertAfter()
  1581. * @see appendTo()
  1582. * @throws QueryPathException
  1583. * Thrown if $data is an unsupported object type.
  1584. */
  1585. public function insertBefore(QueryPath $dest) {
  1586. foreach ($this->matches as $m) $dest->before($m);
  1587. return $this;
  1588. }
  1589. /**
  1590. * Insert the contents of the current QueryPath after the nodes in the
  1591. * destination QueryPath object.
  1592. *
  1593. * @param QueryPath $dest
  1594. * Destination object where the current elements will be deposited.
  1595. * @return QueryPath
  1596. * The present QueryPath, unaltered. Only the destination object is altered.
  1597. * @see after()
  1598. * @see insertBefore()
  1599. * @see append()
  1600. * @throws QueryPathException
  1601. * Thrown if $data is an unsupported object type.
  1602. */
  1603. public function insertAfter(QueryPath $dest) {
  1604. foreach ($this->matches as $m) $dest->after($m);
  1605. return $this;
  1606. }
  1607. /**
  1608. * Insert the given data after each element in the current QueryPath object.
  1609. *
  1610. * This inserts the element as a peer to the currently matched elements.
  1611. * Contrast this with {@link append()}, which inserts the data as children
  1612. * of matched elements.
  1613. *
  1614. * @param mixed $data
  1615. * The data to be appended.
  1616. * @return QueryPath
  1617. * The QueryPath object (with the items inserted).
  1618. * @see before()
  1619. * @see append()
  1620. * @throws QueryPathException
  1621. * Thrown if $data is an unsupported object type.
  1622. */
  1623. public function after($data) {
  1624. $data = $this->prepareInsert($data);
  1625. foreach ($this->matches as $m) {
  1626. $ins = $data->cloneNode(TRUE);
  1627. if (isset($m->nextSibling))
  1628. $m->parentNode->insertBefore($ins, $m->nextSibling);
  1629. else
  1630. $m->parentNode->appendChild($ins);
  1631. }
  1632. return $this;
  1633. }
  1634. /**
  1635. * Replace the existing element(s) in the list with a new one.
  1636. *
  1637. * @param mixed $new
  1638. * A DOMElement or XML in a string. This will replace all elements
  1639. * currently wrapped in the QueryPath object.
  1640. * @return QueryPath
  1641. * The QueryPath object wrapping <b>the items that were removed</b>.
  1642. * This remains consistent with the jQuery API.
  1643. * @see append()
  1644. * @see prepend()
  1645. * @see before()
  1646. * @see after()
  1647. * @see remove()
  1648. * @see replaceAll()
  1649. */
  1650. public function replaceWith($new) {
  1651. $data = $this->prepareInsert($new);
  1652. $found = new SplObjectStorage();
  1653. foreach ($this->matches as $m) {
  1654. $parent = $m->parentNode;
  1655. $parent->insertBefore($data->cloneNode(TRUE), $m);
  1656. $found->attach($parent->removeChild($m));
  1657. }
  1658. $this->setMatches($found);
  1659. return $this;
  1660. }
  1661. /**
  1662. * Remove the parent element from the selected node or nodes.
  1663. *
  1664. * This takes the given list of nodes and "unwraps" them, moving them out of their parent
  1665. * node, and then deleting the parent node.
  1666. *
  1667. * For example, consider this:
  1668. *
  1669. * @code
  1670. * <root><wrapper><content/></wrapper></root>
  1671. * @endcode
  1672. *
  1673. * Now we can run this code:
  1674. * @code
  1675. * qp($xml, 'content')->unwrap();
  1676. * @endcode
  1677. *
  1678. * This will result in:
  1679. *
  1680. * @code
  1681. * <root><content/></root>
  1682. * @endcode
  1683. * This is the opposite of {@link wrap()}.
  1684. *
  1685. * <b>The root element cannot be unwrapped.</b> It has no parents.
  1686. * If you attempt to use unwrap on a root element, this will throw a QueryPathException.
  1687. * (You can, however, "Unwrap" a child that is a direct descendant of the root element. This
  1688. * will remove the root element, and replace the child as the root element. Be careful, though.
  1689. * You cannot set more than one child as a root element.)
  1690. *
  1691. * @return QueryPath
  1692. * The QueryPath object, with the same element(s) selected.
  1693. * @throws QueryPathException
  1694. * An exception is thrown if one attempts to unwrap a root element.
  1695. * @see wrap()
  1696. * @since 2.1
  1697. * @author mbutcher
  1698. */
  1699. public function unwrap() {
  1700. // We do this in two loops in order to
  1701. // capture the case where two matches are
  1702. // under the same parent. Othwerwise we might
  1703. // remove a match before we can move it.
  1704. $parents = new SplObjectStorage();
  1705. foreach ($this->matches as $m) {
  1706. // Cannot unwrap the root element.
  1707. if ($m->isSameNode($m->ownerDocument->documentElement)) {
  1708. throw new QueryPathException('Cannot unwrap the root element.');
  1709. }
  1710. // Move children to peer of parent.
  1711. $parent = $m->parentNode;
  1712. $old = $parent->removeChild($m);
  1713. $parent->parentNode->insertBefore($old, $parent);
  1714. $parents->attach($parent);
  1715. }
  1716. // Now that all the children are moved, we
  1717. // remove all of the parents.
  1718. foreach ($parents as $ele) {
  1719. $ele->parentNode->removeChild($ele);
  1720. }
  1721. return $this;
  1722. }
  1723. /**
  1724. * Wrap each element inside of the given markup.
  1725. *
  1726. * Markup is usually a string, but it can also be a DOMNode, a document
  1727. * fragment, a SimpleXMLElement, or another QueryPath object (in which case
  1728. * the first item in the list will be used.)
  1729. *
  1730. * @param mixed $markup
  1731. * Markup that will wrap each element in the current list.
  1732. * @return QueryPath
  1733. * The QueryPath object with the wrapping changes made.
  1734. * @see wrapAll()
  1735. * @see wrapInner()
  1736. */
  1737. public function wrap($markup) {
  1738. $data = $this->prepareInsert($markup);
  1739. // If the markup passed in is empty, we don't do any wrapping.
  1740. if (empty($data)) {
  1741. return $this;
  1742. }
  1743. foreach ($this->matches as $m) {
  1744. $copy = $data->firstChild->cloneNode(TRUE);
  1745. // XXX: Should be able to avoid doing this over and over.
  1746. if ($copy->hasChildNodes()) {
  1747. $deepest = $this->deepestNode($copy);
  1748. // FIXME: Does this need a different data structure?
  1749. $bottom = $deepest[0];
  1750. }
  1751. else
  1752. $bottom = $copy;
  1753. $parent = $m->parentNode;
  1754. $parent->insertBefore($copy, $m);
  1755. $m = $parent->removeChild($m);
  1756. $bottom->appendChild($m);
  1757. //$parent->appendChild($copy);
  1758. }
  1759. return $this;
  1760. }
  1761. /**
  1762. * Wrap all elements inside of the given markup.
  1763. *
  1764. * So all elements will be grouped together under this single marked up
  1765. * item. This works by first determining the parent element of the first item
  1766. * in the list. It then moves all of the matching elements under the wrapper
  1767. * and inserts the wrapper where that first element was found. (This is in
  1768. * accordance with the way jQuery works.)
  1769. *
  1770. * Markup is usually XML in a string, but it can also be a DOMNode, a document
  1771. * fragment, a SimpleXMLElement, or another QueryPath object (in which case
  1772. * the first item in the list will be used.)
  1773. *
  1774. * @param string $markup
  1775. * Markup that will wrap all elements in the current list.
  1776. * @return QueryPath
  1777. * The QueryPath object with the wrapping changes made.
  1778. * @see wrap()
  1779. * @see wrapInner()
  1780. */
  1781. public function wrapAll($markup) {
  1782. if ($this->matches->count() == 0) return;
  1783. $data = $this->prepareInsert($markup);
  1784. if (empty($data)) {
  1785. return $this;
  1786. }
  1787. if ($data->hasChildNodes()) {
  1788. $deepest = $this->deepestNode($data);
  1789. // FIXME: Does this need fixing?
  1790. $bottom = $deepest[0];
  1791. }
  1792. else
  1793. $bottom = $data;
  1794. $first = $this->getFirstMatch();
  1795. $parent = $first->parentNode;
  1796. $parent->insertBefore($data, $first);
  1797. foreach ($this->matches as $m) {
  1798. $bottom->appendChild($m->parentNode->removeChild($m));
  1799. }
  1800. return $this;
  1801. }
  1802. /**
  1803. * Wrap the child elements of each item in the list with the given markup.
  1804. *
  1805. * Markup is usually a string, but it can also be a DOMNode, a document
  1806. * fragment, a SimpleXMLElement, or another QueryPath object (in which case
  1807. * the first item in the list will be used.)
  1808. *
  1809. * @param string $markup
  1810. * Markup that will wrap children of each element in the current list.
  1811. * @return QueryPath
  1812. * The QueryPath object with the wrapping changes made.
  1813. * @see wrap()
  1814. * @see wrapAll()
  1815. */
  1816. public function wrapInner($markup) {
  1817. $data = $this->prepareInsert($markup);
  1818. // No data? Short circuit.
  1819. if (empty($data)) return $this;
  1820. if ($data->hasChildNodes()) {
  1821. $deepest = $this->deepestNode($data);
  1822. // FIXME: ???
  1823. $bottom = $deepest[0];
  1824. }
  1825. else
  1826. $bottom = $data;
  1827. foreach ($this->matches as $m) {
  1828. if ($m->hasChildNodes()) {
  1829. while($m->firstChild) {
  1830. $kid = $m->removeChild($m->firstChild);
  1831. $bottom->appendChild($kid);
  1832. }
  1833. }
  1834. $m->appendChild($data);
  1835. }
  1836. return $this;
  1837. }
  1838. /**
  1839. * Reduce the set of matches to the deepest child node in the tree.
  1840. *
  1841. * This loops through the matches and looks for the deepest child node of all of
  1842. * the matches. "Deepest", here, is relative to the nodes in the list. It is
  1843. * calculated as the distance from the starting node to the most distant child
  1844. * node. In other words, it is not necessarily the farthest node from the root
  1845. * element, but the farthest note from the matched element.
  1846. *
  1847. * In the case where there are multiple nodes at the same depth, all of the
  1848. * nodes at that depth will be included.
  1849. *
  1850. * @return QueryPath
  1851. * The QueryPath wrapping the single deepest node.
  1852. */
  1853. public function deepest() {
  1854. $deepest = 0;
  1855. $winner = new SplObjectStorage();
  1856. foreach ($this->matches as $m) {
  1857. $local_deepest = 0;
  1858. $local_ele = $this->deepestNode($m, 0, NULL, $local_deepest);
  1859. // Replace with the new deepest.
  1860. if ($local_deepest > $deepest) {
  1861. $winner = new SplObjectStorage();
  1862. foreach ($local_ele as $lele) $winner->attach($lele);
  1863. $deepest = $local_deepest;
  1864. }
  1865. // Augument with other equally deep elements.
  1866. elseif ($local_deepest == $deepest) {
  1867. foreach ($local_ele as $lele)
  1868. $winner->attach($lele);
  1869. }
  1870. }
  1871. $this->setMatches($winner);
  1872. return $this;
  1873. }
  1874. /**
  1875. * A depth-checking function. Typically, it only needs to be
  1876. * invoked with the first parameter. The rest are used for recursion.
  1877. * @see deepest();
  1878. * @param DOMNode $ele
  1879. * The element.
  1880. * @param int $depth
  1881. * The depth guage
  1882. * @param mixed $current
  1883. * The current set.
  1884. * @param DOMNode $deepest
  1885. * A reference to the current deepest node.
  1886. * @return array
  1887. * Returns an array of DOM nodes.
  1888. */
  1889. protected function deepestNode(DOMNode $ele, $depth = 0, $current = NULL, &$deepest = NULL) {
  1890. // FIXME: Should this use SplObjectStorage?
  1891. if (!isset($current)) $current = array($ele);
  1892. if (!isset($deepest)) $deepest = $depth;
  1893. if ($ele->hasChildNodes()) {
  1894. foreach ($ele->childNodes as $child) {
  1895. if ($child->nodeType === XML_ELEMENT_NODE) {
  1896. $current = $this->deepestNode($child, $depth + 1, $current, $deepest);
  1897. }
  1898. }
  1899. }
  1900. elseif ($depth > $deepest) {
  1901. $current = array($ele);
  1902. $deepest = $depth;
  1903. }
  1904. elseif ($depth === $deepest) {
  1905. $current[] = $ele;
  1906. }
  1907. return $current;
  1908. }
  1909. /**
  1910. * Prepare an item for insertion into a DOM.
  1911. *
  1912. * This handles a variety of boilerplate tasks that need doing before an
  1913. * indeterminate object can be inserted into a DOM tree.
  1914. * - If item is a string, this is converted into a document fragment and returned.
  1915. * - If item is a QueryPath, then the first item is retrieved and this call function
  1916. * is called recursivel.
  1917. * - If the item is a DOMNode, it is imported into the current DOM if necessary.
  1918. * - If the item is a SimpleXMLElement, it is converted into a DOM node and then
  1919. * imported.
  1920. *
  1921. * @param mixed $item
  1922. * Item to prepare for insert.
  1923. * @return mixed
  1924. * Returns the prepared item.
  1925. * @throws QueryPathException
  1926. * Thrown if the object passed in is not of a supprted object type.
  1927. */
  1928. protected function prepareInsert($item) {
  1929. if(empty($item)) {
  1930. return;
  1931. }
  1932. elseif (is_string($item)) {
  1933. // If configured to do so, replace all entities.
  1934. if ($this->options['replace_entities']) {
  1935. $item = QueryPathEntities::replaceAllEntities($item);
  1936. }
  1937. $frag = $this->document->createDocumentFragment();
  1938. try {
  1939. set_error_handler(array('QueryPathParseException', 'initializeFromError'), $this->errTypes);
  1940. $frag->appendXML($item);
  1941. }
  1942. // Simulate a finally block.
  1943. catch (Exception $e) {
  1944. restore_error_handler();
  1945. throw $e;
  1946. }
  1947. restore_error_handler();
  1948. return $frag;
  1949. }
  1950. elseif ($item instanceof QueryPath) {
  1951. if ($item->size() == 0)
  1952. return;
  1953. return $this->prepareInsert($item->get(0));
  1954. }
  1955. elseif ($item instanceof DOMNode) {
  1956. if ($item->ownerDocument !== $this->document) {
  1957. // Deep clone this and attach it to this document
  1958. $item = $this->document->importNode($item, TRUE);
  1959. }
  1960. return $item;
  1961. }
  1962. elseif ($item instanceof SimpleXMLElement) {
  1963. $element = dom_import_simplexml($item);
  1964. return $this->document->importNode($element, TRUE);
  1965. }
  1966. // What should we do here?
  1967. //var_dump($item);
  1968. throw new QueryPathException("Cannot prepare item of unsupported type: " . gettype($item));
  1969. }
  1970. /**
  1971. * The tag name of the first element in the list.
  1972. *
  1973. * This returns the tag name of the first element in the list of matches. If
  1974. * the list is empty, an empty string will be used.
  1975. *
  1976. * @see replaceAll()
  1977. * @see replaceWith()
  1978. * @return string
  1979. * The tag name of the first element in the list.
  1980. */
  1981. public function tag() {
  1982. return ($this->size() > 0) ? $this->getFirstMatch()->tagName : '';
  1983. }
  1984. /**
  1985. * Remove any items from the list if they match the selector.
  1986. *
  1987. * In other words, each item that matches the selector will be remove
  1988. * from the DOM document. The returned QueryPath wraps the list of
  1989. * removed elements.
  1990. *
  1991. * If no selector is specified, this will remove all current matches from
  1992. * the document.
  1993. *
  1994. * @param string $selector
  1995. * A CSS Selector.
  1996. * @return QueryPath
  1997. * The Query path wrapping a list of removed items.
  1998. * @see replaceAll()
  1999. * @see replaceWith()
  2000. * @see removeChildren()
  2001. */
  2002. public function remove($selector = NULL) {
  2003. if(!empty($selector)) {
  2004. // Do a non-destructive find.
  2005. $query = new QueryPathCssEventHandler($this->matches);
  2006. $query->find($selector);
  2007. $matches = $query->getMatches();
  2008. }
  2009. else {
  2010. $matches = $this->matches;
  2011. }
  2012. $found = new SplObjectStorage();
  2013. foreach ($matches as $item) {
  2014. // The item returned is (according to docs) different from
  2015. // the one passed in, so we have to re-store it.
  2016. $found->attach($item->parentNode->removeChild($item));
  2017. }
  2018. // Return a clone QueryPath with just the removed items. If
  2019. // no items are found, this will return an empty QueryPath.
  2020. return count($found) == 0 ? new QueryPath() : new QueryPath($found);
  2021. }
  2022. /**
  2023. * This replaces everything that matches the selector with the first value
  2024. * in the current list.
  2025. *
  2026. * This is the reverse of replaceWith.
  2027. *
  2028. * Unlike jQuery, QueryPath cannot assume a default document. Consequently,
  2029. * you must specify the intended destination document. If it is omitted, the
  2030. * present document is assumed to be tthe document. However, that can result
  2031. * in undefined behavior if the selector and the replacement are not sufficiently
  2032. * distinct.
  2033. *
  2034. * @param string $selector
  2035. * The selector.
  2036. * @param DOMDocument $document
  2037. * The destination document.
  2038. * @return QueryPath
  2039. * The QueryPath wrapping the modified document.
  2040. * @deprecated Due to the fact that this is not a particularly friendly method,
  2041. * and that it can be easily replicated using {@see replaceWith()}, it is to be
  2042. * considered deprecated.
  2043. * @see remove()
  2044. * @see replaceWith()
  2045. */
  2046. public function replaceAll($selector, DOMDocument $document) {
  2047. $replacement = $this->size() > 0 ? $this->getFirstMatch() : $this->document->createTextNode('');
  2048. $c = new QueryPathCssEventHandler($document);
  2049. $c->find($selector);
  2050. $temp = $c->getMatches();
  2051. foreach ($temp as $item) {
  2052. $node = $replacement->cloneNode();
  2053. $node = $document->importNode($node);
  2054. $item->parentNode->replaceChild($node, $item);
  2055. }
  2056. return qp($document, NULL, $this->options);
  2057. }
  2058. /**
  2059. * Add more elements to the current set of matches.
  2060. *
  2061. * This begins the new query at the top of the DOM again. The results found
  2062. * when running this selector are then merged into the existing results. In
  2063. * this way, you can add additional elements to the existing set.
  2064. *
  2065. * @param string $selector
  2066. * A valid selector.
  2067. * @return QueryPath
  2068. * The QueryPath object with the newly added elements.
  2069. * @see append()
  2070. * @see after()
  2071. * @see andSelf()
  2072. * @see end()
  2073. */
  2074. public function add($selector) {
  2075. // This is destructive, so we need to set $last:
  2076. $this->last = $this->matches;
  2077. foreach (qp($this->document, $selector, $this->options)->get() as $item)
  2078. $this->matches->attach($item);
  2079. return $this;
  2080. }
  2081. /**
  2082. * Revert to the previous set of matches.
  2083. *
  2084. * This will revert back to the last set of matches (before the last
  2085. * "destructive" set of operations). This undoes any change made to the set of
  2086. * matched objects. Functions like find() and filter() change the
  2087. * list of matched objects. The end() function will revert back to the last set of
  2088. * matched items.
  2089. *
  2090. * Note that functions that modify the document, but do not change the list of
  2091. * matched objects, are not "destructive". Thus, calling append('something')->end()
  2092. * will not undo the append() call.
  2093. *
  2094. * Only one level of changes is stored. Reverting beyond that will result in
  2095. * an empty set of matches. Example:
  2096. *
  2097. * @code
  2098. * // The line below returns the same thing as qp(document, 'p');
  2099. * qp(document, 'p')->find('div')->end();
  2100. * // This returns an empty array:
  2101. * qp(document, 'p')->end();
  2102. * // This returns an empty array:
  2103. * qp(document, 'p')->find('div')->find('span')->end()->end();
  2104. * @endcode
  2105. *
  2106. * The last one returns an empty array because only one level of changes is stored.
  2107. *
  2108. * @return QueryPath
  2109. * A QueryPath object reflecting the list of matches prior to the last destructive
  2110. * operation.
  2111. * @see andSelf()
  2112. * @see add()
  2113. */
  2114. public function end() {
  2115. // Note that this does not use setMatches because it must set the previous
  2116. // set of matches to empty array.
  2117. $this->matches = $this->last;
  2118. $this->last = new SplObjectStorage();
  2119. return $this;
  2120. }
  2121. /**
  2122. * Combine the current and previous set of matched objects.
  2123. *
  2124. * Example:
  2125. *
  2126. * @code
  2127. * qp(document, 'p')->find('div')->andSelf();
  2128. * @endcode
  2129. *
  2130. * The code above will contain a list of all p elements and all div elements that
  2131. * are beneath p elements.
  2132. *
  2133. * @see end();
  2134. * @return QueryPath
  2135. * A QueryPath object with the results of the last two "destructive" operations.
  2136. * @see add()
  2137. * @see end()
  2138. */
  2139. public function andSelf() {
  2140. // This is destructive, so we need to set $last:
  2141. $last = $this->matches;
  2142. foreach ($this->last as $item) $this->matches->attach($item);
  2143. $this->last = $last;
  2144. return $this;
  2145. }
  2146. /**
  2147. * Remove all child nodes.
  2148. *
  2149. * This is equivalent to jQuery's empty() function. (However, empty() is a
  2150. * PHP built-in, and cannot be used as a method name.)
  2151. *
  2152. * @return QueryPath
  2153. * The QueryPath object with the child nodes removed.
  2154. * @see replaceWith()
  2155. * @see replaceAll()
  2156. * @see remove()
  2157. */
  2158. public function removeChildren() {
  2159. foreach ($this->matches as $m) {
  2160. while($kid = $m->firstChild) {
  2161. $m->removeChild($kid);
  2162. }
  2163. }
  2164. return $this;
  2165. }
  2166. /**
  2167. * Get the children of the elements in the QueryPath object.
  2168. *
  2169. * If a selector is provided, the list of children will be filtered through
  2170. * the selector.
  2171. *
  2172. * @param string $selector
  2173. * A valid selector.
  2174. * @return QueryPath
  2175. * A QueryPath wrapping all of the children.
  2176. * @see removeChildren()
  2177. * @see parent()
  2178. * @see parents()
  2179. * @see next()
  2180. * @see prev()
  2181. */
  2182. public function children($selector = NULL) {
  2183. $found = new SplObjectStorage();
  2184. foreach ($this->matches as $m) {
  2185. foreach($m->childNodes as $c) {
  2186. if ($c->nodeType == XML_ELEMENT_NODE) $found->attach($c);
  2187. }
  2188. }
  2189. if (empty($selector)) {
  2190. $this->setMatches($found);
  2191. }
  2192. else {
  2193. $this->matches = $found; // Don't buffer this. It is temporary.
  2194. $this->filter($selector);
  2195. }
  2196. return $this;
  2197. }
  2198. /**
  2199. * Get all child nodes (not just elements) of all items in the matched set.
  2200. *
  2201. * It gets only the immediate children, not all nodes in the subtree.
  2202. *
  2203. * This does not process iframes. Xinclude processing is dependent on the
  2204. * DOM implementation and configuration.
  2205. *
  2206. * @return QueryPath
  2207. * A QueryPath object wrapping all child nodes for all elements in the
  2208. * QueryPath object.
  2209. * @see find()
  2210. * @see text()
  2211. * @see html()
  2212. * @see innerHTML()
  2213. * @see xml()
  2214. * @see innerXML()
  2215. */
  2216. public function contents() {
  2217. $found = new SplObjectStorage();
  2218. foreach ($this->matches as $m) {
  2219. if (empty($m->childNodes)) continue; // Issue #51
  2220. foreach ($m->childNodes as $c) {
  2221. $found->attach($c);
  2222. }
  2223. }
  2224. $this->setMatches($found);
  2225. return $this;
  2226. }
  2227. /**
  2228. * Get a list of siblings for elements currently wrapped by this object.
  2229. *
  2230. * This will compile a list of every sibling of every element in the
  2231. * current list of elements.
  2232. *
  2233. * Note that if two siblings are present in the QueryPath object to begin with,
  2234. * then both will be returned in the matched set, since they are siblings of each
  2235. * other. In other words,if the matches contain a and b, and a and b are siblings of
  2236. * each other, than running siblings will return a set that contains
  2237. * both a and b.
  2238. *
  2239. * @param string $selector
  2240. * If the optional selector is provided, siblings will be filtered through
  2241. * this expression.
  2242. * @return QueryPath
  2243. * The QueryPath containing the matched siblings.
  2244. * @see contents()
  2245. * @see children()
  2246. * @see parent()
  2247. * @see parents()
  2248. */
  2249. public function siblings($selector = NULL) {
  2250. $found = new SplObjectStorage();
  2251. foreach ($this->matches as $m) {
  2252. $parent = $m->parentNode;
  2253. foreach ($parent->childNodes as $n) {
  2254. if ($n->nodeType == XML_ELEMENT_NODE && $n !== $m) {
  2255. $found->attach($n);
  2256. }
  2257. }
  2258. }
  2259. if (empty($selector)) {
  2260. $this->setMatches($found);
  2261. }
  2262. else {
  2263. $this->matches = $found; // Don't buffer this. It is temporary.
  2264. $this->filter($selector);
  2265. }
  2266. return $this;
  2267. }
  2268. /**
  2269. * Find the closest element matching the selector.
  2270. *
  2271. * This finds the closest match in the ancestry chain. It first checks the
  2272. * present element. If the present element does not match, this traverses up
  2273. * the ancestry chain (e.g. checks each parent) looking for an item that matches.
  2274. *
  2275. * It is provided for jQuery 1.3 compatibility.
  2276. * @param string $selector
  2277. * A CSS Selector to match.
  2278. * @return QueryPath
  2279. * The set of matches.
  2280. * @since 2.0
  2281. */
  2282. public function closest($selector) {
  2283. $found = new SplObjectStorage();
  2284. foreach ($this->matches as $m) {
  2285. if (qp($m, NULL, $this->options)->is($selector) > 0) {
  2286. $found->attach($m);
  2287. }
  2288. else {
  2289. while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
  2290. $m = $m->parentNode;
  2291. // Is there any case where parent node is not an element?
  2292. if ($m->nodeType === XML_ELEMENT_NODE && qp($m, NULL, $this->options)->is($selector) > 0) {
  2293. $found->attach($m);
  2294. break;
  2295. }
  2296. }
  2297. }
  2298. }
  2299. $this->setMatches($found);
  2300. return $this;
  2301. }
  2302. /**
  2303. * Get the immediate parent of each element in the QueryPath.
  2304. *
  2305. * If a selector is passed, this will return the nearest matching parent for
  2306. * each element in the QueryPath.
  2307. *
  2308. * @param string $selector
  2309. * A valid CSS3 selector.
  2310. * @return QueryPath
  2311. * A QueryPath object wrapping the matching parents.
  2312. * @see children()
  2313. * @see siblings()
  2314. * @see parents()
  2315. */
  2316. public function parent($selector = NULL) {
  2317. $found = new SplObjectStorage();
  2318. foreach ($this->matches as $m) {
  2319. while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
  2320. $m = $m->parentNode;
  2321. // Is there any case where parent node is not an element?
  2322. if ($m->nodeType === XML_ELEMENT_NODE) {
  2323. if (!empty($selector)) {
  2324. if (qp($m, NULL, $this->options)->is($selector) > 0) {
  2325. $found->attach($m);
  2326. break;
  2327. }
  2328. }
  2329. else {
  2330. $found->attach($m);
  2331. break;
  2332. }
  2333. }
  2334. }
  2335. }
  2336. $this->setMatches($found);
  2337. return $this;
  2338. }
  2339. /**
  2340. * Get all ancestors of each element in the QueryPath.
  2341. *
  2342. * If a selector is present, only matching ancestors will be retrieved.
  2343. *
  2344. * @see parent()
  2345. * @param string $selector
  2346. * A valid CSS 3 Selector.
  2347. * @return QueryPath
  2348. * A QueryPath object containing the matching ancestors.
  2349. * @see siblings()
  2350. * @see children()
  2351. */
  2352. public function parents($selector = NULL) {
  2353. $found = new SplObjectStorage();
  2354. foreach ($this->matches as $m) {
  2355. while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
  2356. $m = $m->parentNode;
  2357. // Is there any case where parent node is not an element?
  2358. if ($m->nodeType === XML_ELEMENT_NODE) {
  2359. if (!empty($selector)) {
  2360. if (qp($m, NULL, $this->options)->is($selector) > 0)
  2361. $found->attach($m);
  2362. }
  2363. else
  2364. $found->attach($m);
  2365. }
  2366. }
  2367. }
  2368. $this->setMatches($found);
  2369. return $this;
  2370. }
  2371. /**
  2372. * Set or get the markup for an element.
  2373. *
  2374. * If $markup is set, then the giving markup will be injected into each
  2375. * item in the set. All other children of that node will be deleted, and this
  2376. * new code will be the only child or children. The markup MUST BE WELL FORMED.
  2377. *
  2378. * If no markup is given, this will return a string representing the child
  2379. * markup of the first node.
  2380. *
  2381. * <b>Important:</b> This differs from jQuery's html() function. This function
  2382. * returns <i>the current node</i> and all of its children. jQuery returns only
  2383. * the children. This means you do not need to do things like this:
  2384. * @code$qp->parent()->html()@endcode.
  2385. *
  2386. * By default, this is HTML 4.01, not XHTML. Use {@link xml()} for XHTML.
  2387. *
  2388. * @param string $markup
  2389. * The text to insert.
  2390. * @return mixed
  2391. * A string if no markup was passed, or a QueryPath if markup was passed.
  2392. * @see xml()
  2393. * @see text()
  2394. * @see contents()
  2395. */
  2396. public function html($markup = NULL) {
  2397. if (isset($markup)) {
  2398. if ($this->options['replace_entities']) {
  2399. $markup = QueryPathEntities::replaceAllEntities($markup);
  2400. }
  2401. // Parse the HTML and insert it into the DOM
  2402. //$doc = DOMDocument::loadHTML($markup);
  2403. $doc = $this->document->createDocumentFragment();
  2404. $doc->appendXML($markup);
  2405. $this->removeChildren();
  2406. $this->append($doc);
  2407. return $this;
  2408. }
  2409. $length = $this->size();
  2410. if ($length == 0) {
  2411. return NULL;
  2412. }
  2413. // Only return the first item -- that's what JQ does.
  2414. $first = $this->getFirstMatch();
  2415. // Catch cases where first item is not a legit DOM object.
  2416. if (!($first instanceof DOMNode)) {
  2417. return NULL;
  2418. }
  2419. // Added by eabrand.
  2420. if(!$first->ownerDocument->documentElement) {
  2421. return NULL;
  2422. }
  2423. if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
  2424. return $this->document->saveHTML();
  2425. }
  2426. // saveHTML cannot take a node and serialize it.
  2427. return $this->document->saveXML($first);
  2428. }
  2429. /**
  2430. * Fetch the HTML contents INSIDE of the first QueryPath item.
  2431. *
  2432. * <b>This behaves the way jQuery's @codehtml()@endcode function behaves.</b>
  2433. *
  2434. * This gets all children of the first match in QueryPath.
  2435. *
  2436. * Consider this fragment:
  2437. * @code
  2438. * <div>
  2439. * test <p>foo</p> test
  2440. * </div>
  2441. * @endcode
  2442. *
  2443. * We can retrieve just the contents of this code by doing something like
  2444. * this:
  2445. * @code
  2446. * qp($xml, 'div')->innerHTML();
  2447. * @endcode
  2448. *
  2449. * This would return the following:
  2450. * @codetest <p>foo</p> test@endcode
  2451. *
  2452. * @return string
  2453. * Returns a string representation of the child nodes of the first
  2454. * matched element.
  2455. * @see html()
  2456. * @see innerXML()
  2457. * @see innerXHTML()
  2458. * @since 2.0
  2459. */
  2460. public function innerHTML() {
  2461. return $this->innerXML();
  2462. }
  2463. /**
  2464. * Fetch child (inner) nodes of the first match.
  2465. *
  2466. * This will return the children of the present match. For an example,
  2467. * see {@link innerHTML()}.
  2468. *
  2469. * @see innerHTML()
  2470. * @see innerXML()
  2471. * @return string
  2472. * Returns a string of XHTML that represents the children of the present
  2473. * node.
  2474. * @since 2.0
  2475. */
  2476. public function innerXHTML() {
  2477. $length = $this->size();
  2478. if ($length == 0) {
  2479. return NULL;
  2480. }
  2481. // Only return the first item -- that's what JQ does.
  2482. $first = $this->getFirstMatch();
  2483. // Catch cases where first item is not a legit DOM object.
  2484. if (!($first instanceof DOMNode)) {
  2485. return NULL;
  2486. }
  2487. elseif (!$first->hasChildNodes()) {
  2488. return '';
  2489. }
  2490. $buffer = '';
  2491. foreach ($first->childNodes as $child) {
  2492. $buffer .= $this->document->saveXML($child, LIBXML_NOEMPTYTAG);
  2493. }
  2494. return $buffer;
  2495. }
  2496. /**
  2497. * Fetch child (inner) nodes of the first match.
  2498. *
  2499. * This will return the children of the present match. For an example,
  2500. * see {@link innerHTML()}.
  2501. *
  2502. * @see innerHTML()
  2503. * @see innerXHTML()
  2504. * @return string
  2505. * Returns a string of XHTML that represents the children of the present
  2506. * node.
  2507. * @since 2.0
  2508. */
  2509. public function innerXML() {
  2510. $length = $this->size();
  2511. if ($length == 0) {
  2512. return NULL;
  2513. }
  2514. // Only return the first item -- that's what JQ does.
  2515. $first = $this->getFirstMatch();
  2516. // Catch cases where first item is not a legit DOM object.
  2517. if (!($first instanceof DOMNode)) {
  2518. return NULL;
  2519. }
  2520. elseif (!$first->hasChildNodes()) {
  2521. return '';
  2522. }
  2523. $buffer = '';
  2524. foreach ($first->childNodes as $child) {
  2525. $buffer .= $this->document->saveXML($child);
  2526. }
  2527. return $buffer;
  2528. }
  2529. /**
  2530. * Retrieve the text of each match and concatenate them with the given separator.
  2531. *
  2532. * This has the effect of looping through all children, retrieving their text
  2533. * content, and then concatenating the text with a separator.
  2534. *
  2535. * @param string $sep
  2536. * The string used to separate text items. The default is a comma followed by a
  2537. * space.
  2538. * @param boolean $filterEmpties
  2539. * If this is true, empty items will be ignored.
  2540. * @return string
  2541. * The text contents, concatenated together with the given separator between
  2542. * every pair of items.
  2543. * @see implode()
  2544. * @see text()
  2545. * @since 2.0
  2546. */
  2547. public function textImplode($sep = ', ', $filterEmpties = TRUE) {
  2548. $tmp = array();
  2549. foreach ($this->matches as $m) {
  2550. $txt = $m->textContent;
  2551. $trimmed = trim($txt);
  2552. // If filter empties out, then we only add items that have content.
  2553. if ($filterEmpties) {
  2554. if (strlen($trimmed) > 0) $tmp[] = $txt;
  2555. }
  2556. // Else add all content, even if it's empty.
  2557. else {
  2558. $tmp[] = $txt;
  2559. }
  2560. }
  2561. return implode($sep, $tmp);
  2562. }
  2563. /**
  2564. * Get the text contents from just child elements.
  2565. *
  2566. * This is a specialized variant of textImplode() that implodes text for just the
  2567. * child elements of the current element.
  2568. *
  2569. * @param string $separator
  2570. * The separator that will be inserted between found text content.
  2571. * @return string
  2572. * The concatenated values of all children.
  2573. */
  2574. function childrenText($separator = ' ') {
  2575. // Branch makes it non-destructive.
  2576. return $this->branch()->xpath('descendant::text()')->textImplode($separator);
  2577. }
  2578. /**
  2579. * Get or set the text contents of a node.
  2580. * @param string $text
  2581. * If this is not NULL, this value will be set as the text of the node. It
  2582. * will replace any existing content.
  2583. * @return mixed
  2584. * A QueryPath if $text is set, or the text content if no text
  2585. * is passed in as a pram.
  2586. * @see html()
  2587. * @see xml()
  2588. * @see contents()
  2589. */
  2590. public function text($text = NULL) {
  2591. if (isset($text)) {
  2592. $this->removeChildren();
  2593. $textNode = $this->document->createTextNode($text);
  2594. foreach ($this->matches as $m) $m->appendChild($textNode);
  2595. return $this;
  2596. }
  2597. // Returns all text as one string:
  2598. $buf = '';
  2599. foreach ($this->matches as $m) $buf .= $m->textContent;
  2600. return $buf;
  2601. }
  2602. /**
  2603. * Get or set the text before each selected item.
  2604. *
  2605. * If $text is passed in, the text is inserted before each currently selected item.
  2606. *
  2607. * If no text is given, this will return the concatenated text after each selected element.
  2608. *
  2609. * @code
  2610. * <?php
  2611. * $xml = '<?xml version="1.0"?><root>Foo<a>Bar</a><b/></root>';
  2612. *
  2613. * // This will return 'Foo'
  2614. * qp($xml, 'a')->textBefore();
  2615. *
  2616. * // This will insert 'Baz' right before <b/>.
  2617. * qp($xml, 'b')->textBefore('Baz');
  2618. * ?>
  2619. * @endcode
  2620. *
  2621. * @param string $text
  2622. * If this is set, it will be inserted before each node in the current set of
  2623. * selected items.
  2624. * @return mixed
  2625. * Returns the QueryPath object if $text was set, and returns a string (possibly empty)
  2626. * if no param is passed.
  2627. */
  2628. public function textBefore($text = NULL) {
  2629. if (isset($text)) {
  2630. $textNode = $this->document->createTextNode($text);
  2631. return $this->before($textNode);
  2632. }
  2633. $buffer = '';
  2634. foreach ($this->matches as $m) {
  2635. $p = $m;
  2636. while (isset($p->previousSibling) && $p->previousSibling->nodeType == XML_TEXT_NODE) {
  2637. $p = $p->previousSibling;
  2638. $buffer .= $p->textContent;
  2639. }
  2640. }
  2641. return $buffer;
  2642. }
  2643. public function textAfter($text = NULL) {
  2644. if (isset($text)) {
  2645. $textNode = $this->document->createTextNode($text);
  2646. return $this->after($textNode);
  2647. }
  2648. $buffer = '';
  2649. foreach ($this->matches as $m) {
  2650. $n = $m;
  2651. while (isset($n->nextSibling) && $n->nextSibling->nodeType == XML_TEXT_NODE) {
  2652. $n = $n->nextSibling;
  2653. $buffer .= $n->textContent;
  2654. }
  2655. }
  2656. return $buffer;
  2657. }
  2658. /**
  2659. * Set or get the value of an element's 'value' attribute.
  2660. *
  2661. * The 'value' attribute is common in HTML form elements. This is a
  2662. * convenience function for accessing the values. Since this is not common
  2663. * task on the server side, this method may be removed in future releases. (It
  2664. * is currently provided for jQuery compatibility.)
  2665. *
  2666. * If a value is provided in the params, then the value will be set for all
  2667. * matches. If no params are given, then the value of the first matched element
  2668. * will be returned. This may be NULL.
  2669. *
  2670. * @deprecated Just use attr(). There's no reason to use this on the server.
  2671. * @see attr()
  2672. * @param string $value
  2673. * @return mixed
  2674. * Returns a QueryPath if a string was passed in, and a string if no string
  2675. * was passed in. In the later case, an error will produce NULL.
  2676. */
  2677. public function val($value = NULL) {
  2678. if (isset($value)) {
  2679. $this->attr('value', $value);
  2680. return $this;
  2681. }
  2682. return $this->attr('value');
  2683. }
  2684. /**
  2685. * Set or get XHTML markup for an element or elements.
  2686. *
  2687. * This differs from {@link html()} in that it processes (and produces)
  2688. * strictly XML 1.0 compliant markup.
  2689. *
  2690. * Like {@link xml()} and {@link html()}, this functions as both a
  2691. * setter and a getter.
  2692. *
  2693. * This is a convenience function for fetching HTML in XML format.
  2694. * It does no processing of the markup (such as schema validation).
  2695. * @param string $markup
  2696. * A string containing XML data.
  2697. * @return mixed
  2698. * If markup is passed in, a QueryPath is returned. If no markup is passed
  2699. * in, XML representing the first matched element is returned.
  2700. * @see html()
  2701. * @see innerXHTML()
  2702. */
  2703. public function xhtml($markup = NULL) {
  2704. // XXX: This is a minor reworking of the original xml() method.
  2705. // This should be refactored, probably.
  2706. // See http://github.com/technosophos/querypath/issues#issue/10
  2707. $omit_xml_decl = $this->options['omit_xml_declaration'];
  2708. if ($markup === TRUE) {
  2709. // Basically, we handle the special case where we don't
  2710. // want the XML declaration to be displayed.
  2711. $omit_xml_decl = TRUE;
  2712. }
  2713. elseif (isset($markup)) {
  2714. return $this->xml($markup);
  2715. }
  2716. $length = $this->size();
  2717. if ($length == 0) {
  2718. return NULL;
  2719. }
  2720. // Only return the first item -- that's what JQ does.
  2721. $first = $this->getFirstMatch();
  2722. // Catch cases where first item is not a legit DOM object.
  2723. if (!($first instanceof DOMNode)) {
  2724. return NULL;
  2725. }
  2726. if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
  2727. // Has the unfortunate side-effect of stripping doctype.
  2728. //$text = ($omit_xml_decl ? $this->document->saveXML($first->ownerDocument->documentElement, LIBXML_NOEMPTYTAG) : $this->document->saveXML(NULL, LIBXML_NOEMPTYTAG));
  2729. $text = $this->document->saveXML(NULL, LIBXML_NOEMPTYTAG);
  2730. }
  2731. else {
  2732. $text = $this->document->saveXML($first, LIBXML_NOEMPTYTAG);
  2733. }
  2734. // Issue #47: Using the old trick for removing the XML tag also removed the
  2735. // doctype. So we remove it with a regex:
  2736. if ($omit_xml_decl) {
  2737. $text = preg_replace('/<\?xml\s[^>]*\?>/', '', $text);
  2738. }
  2739. // This is slightly lenient: It allows for cases where code incorrectly places content
  2740. // inside of these supposedly unary elements.
  2741. $unary = '/<(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)(?(?=\s)([^>\/]+))><\/[^>]*>/i';
  2742. $text = preg_replace($unary, '<\\1\\2 />', $text);
  2743. // Experimental: Support for enclosing CDATA sections with comments to be both XML compat
  2744. // and HTML 4/5 compat
  2745. $cdata = '/(<!\[CDATA\[|\]\]>)/i';
  2746. $replace = $this->options['escape_xhtml_js_css_sections'];
  2747. $text = preg_replace($cdata, $replace, $text);
  2748. return $text;
  2749. }
  2750. /**
  2751. * Set or get the XML markup for an element or elements.
  2752. *
  2753. * Like {@link html()}, this functions in both a setter and a getter mode.
  2754. *
  2755. * In setter mode, the string passed in will be parsed and then appended to the
  2756. * elements wrapped by this QueryPath object.When in setter mode, this parses
  2757. * the XML using the DOMFragment parser. For that reason, an XML declaration
  2758. * is not necessary.
  2759. *
  2760. * In getter mode, the first element wrapped by this QueryPath object will be
  2761. * converted to an XML string and returned.
  2762. *
  2763. * @param string $markup
  2764. * A string containing XML data.
  2765. * @return mixed
  2766. * If markup is passed in, a QueryPath is returned. If no markup is passed
  2767. * in, XML representing the first matched element is returned.
  2768. * @see xhtml()
  2769. * @see html()
  2770. * @see text()
  2771. * @see content()
  2772. * @see innerXML()
  2773. */
  2774. public function xml($markup = NULL) {
  2775. $omit_xml_decl = $this->options['omit_xml_declaration'];
  2776. if ($markup === TRUE) {
  2777. // Basically, we handle the special case where we don't
  2778. // want the XML declaration to be displayed.
  2779. $omit_xml_decl = TRUE;
  2780. }
  2781. elseif (isset($markup)) {
  2782. if ($this->options['replace_entities']) {
  2783. $markup = QueryPathEntities::replaceAllEntities($markup);
  2784. }
  2785. $doc = $this->document->createDocumentFragment();
  2786. $doc->appendXML($markup);
  2787. $this->removeChildren();
  2788. $this->append($doc);
  2789. return $this;
  2790. }
  2791. $length = $this->size();
  2792. if ($length == 0) {
  2793. return NULL;
  2794. }
  2795. // Only return the first item -- that's what JQ does.
  2796. $first = $this->getFirstMatch();
  2797. // Catch cases where first item is not a legit DOM object.
  2798. if (!($first instanceof DOMNode)) {
  2799. return NULL;
  2800. }
  2801. if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
  2802. return ($omit_xml_decl ? $this->document->saveXML($first->ownerDocument->documentElement) : $this->document->saveXML());
  2803. }
  2804. return $this->document->saveXML($first);
  2805. }
  2806. /**
  2807. * Send the XML document to the client.
  2808. *
  2809. * Write the document to a file path, if given, or
  2810. * to stdout (usually the client).
  2811. *
  2812. * This prints the entire document.
  2813. *
  2814. * @param string $path
  2815. * The path to the file into which the XML should be written. if
  2816. * this is NULL, data will be written to STDOUT, which is usually
  2817. * sent to the remote browser.
  2818. * @param int $options
  2819. * (As of QueryPath 2.1) Pass libxml options to the saving mechanism.
  2820. * @return QueryPath
  2821. * The QueryPath object, unmodified.
  2822. * @see xml()
  2823. * @see innerXML()
  2824. * @see writeXHTML()
  2825. * @throws Exception
  2826. * In the event that a file cannot be written, an Exception will be thrown.
  2827. */
  2828. public function writeXML($path = NULL, $options = NULL) {
  2829. if ($path == NULL) {
  2830. print $this->document->saveXML(NULL, $options);
  2831. }
  2832. else {
  2833. try {
  2834. set_error_handler(array('QueryPathIOException', 'initializeFromError'));
  2835. $this->document->save($path, $options);
  2836. }
  2837. catch (Exception $e) {
  2838. restore_error_handler();
  2839. throw $e;
  2840. }
  2841. restore_error_handler();
  2842. }
  2843. return $this;
  2844. }
  2845. /**
  2846. * Writes HTML to output.
  2847. *
  2848. * HTML is formatted as HTML 4.01, without strict XML unary tags. This is for
  2849. * legacy HTML content. Modern XHTML should be written using {@link toXHTML()}.
  2850. *
  2851. * Write the document to stdout (usually the client) or to a file.
  2852. *
  2853. * @param string $path
  2854. * The path to the file into which the XML should be written. if
  2855. * this is NULL, data will be written to STDOUT, which is usually
  2856. * sent to the remote browser.
  2857. * @return QueryPath
  2858. * The QueryPath object, unmodified.
  2859. * @see html()
  2860. * @see innerHTML()
  2861. * @throws Exception
  2862. * In the event that a file cannot be written, an Exception will be thrown.
  2863. */
  2864. public function writeHTML($path = NULL) {
  2865. if ($path == NULL) {
  2866. print $this->document->saveHTML();
  2867. }
  2868. else {
  2869. try {
  2870. set_error_handler(array('QueryPathParseException', 'initializeFromError'));
  2871. $this->document->saveHTMLFile($path);
  2872. }
  2873. catch (Exception $e) {
  2874. restore_error_handler();
  2875. throw $e;
  2876. }
  2877. restore_error_handler();
  2878. }
  2879. return $this;
  2880. }
  2881. /**
  2882. * Write an XHTML file to output.
  2883. *
  2884. * Typically, you should use this instead of {@link writeHTML()}.
  2885. *
  2886. * Currently, this functions identically to {@link toXML()} <i>except that</i>
  2887. * it always uses closing tags (e.g. always @code<script></script>@endcode,
  2888. * never @code<script/>@endcode). It will
  2889. * write the file as well-formed XML. No XHTML schema validation is done.
  2890. *
  2891. * @see writeXML()
  2892. * @see xml()
  2893. * @see writeHTML()
  2894. * @see innerXHTML()
  2895. * @see xhtml()
  2896. * @param string $path
  2897. * The filename of the file to write to.
  2898. * @return QueryPath
  2899. * Returns the QueryPath, unmodified.
  2900. * @throws Exception
  2901. * In the event that the output file cannot be written, an exception is
  2902. * thrown.
  2903. * @since 2.0
  2904. */
  2905. public function writeXHTML($path = NULL) {
  2906. return $this->writeXML($path, LIBXML_NOEMPTYTAG);
  2907. /*
  2908. if ($path == NULL) {
  2909. print $this->document->saveXML(NULL, LIBXML_NOEMPTYTAG);
  2910. }
  2911. else {
  2912. try {
  2913. set_error_handler(array('QueryPathIOException', 'initializeFromError'));
  2914. $this->document->save($path, LIBXML_NOEMPTYTAG);
  2915. }
  2916. catch (Exception $e) {
  2917. restore_error_handler();
  2918. throw $e;
  2919. }
  2920. restore_error_handler();
  2921. }
  2922. return $this;
  2923. */
  2924. }
  2925. /**
  2926. * Get the next sibling of each element in the QueryPath.
  2927. *
  2928. * If a selector is provided, the next matching sibling will be returned.
  2929. *
  2930. * @param string $selector
  2931. * A CSS3 selector.
  2932. * @return QueryPath
  2933. * The QueryPath object.
  2934. * @see nextAll()
  2935. * @see prev()
  2936. * @see children()
  2937. * @see contents()
  2938. * @see parent()
  2939. * @see parents()
  2940. */
  2941. public function next($selector = NULL) {
  2942. $found = new SplObjectStorage();
  2943. foreach ($this->matches as $m) {
  2944. while (isset($m->nextSibling)) {
  2945. $m = $m->nextSibling;
  2946. if ($m->nodeType === XML_ELEMENT_NODE) {
  2947. if (!empty($selector)) {
  2948. if (qp($m, NULL, $this->options)->is($selector) > 0) {
  2949. $found->attach($m);
  2950. break;
  2951. }
  2952. }
  2953. else {
  2954. $found->attach($m);
  2955. break;
  2956. }
  2957. }
  2958. }
  2959. }
  2960. $this->setMatches($found);
  2961. return $this;
  2962. }
  2963. /**
  2964. * Get all siblings after an element.
  2965. *
  2966. * For each element in the QueryPath, get all siblings that appear after
  2967. * it. If a selector is passed in, then only siblings that match the
  2968. * selector will be included.
  2969. *
  2970. * @param string $selector
  2971. * A valid CSS 3 selector.
  2972. * @return QueryPath
  2973. * The QueryPath object, now containing the matching siblings.
  2974. * @see next()
  2975. * @see prevAll()
  2976. * @see children()
  2977. * @see siblings()
  2978. */
  2979. public function nextAll($selector = NULL) {
  2980. $found = new SplObjectStorage();
  2981. foreach ($this->matches as $m) {
  2982. while (isset($m->nextSibling)) {
  2983. $m = $m->nextSibling;
  2984. if ($m->nodeType === XML_ELEMENT_NODE) {
  2985. if (!empty($selector)) {
  2986. if (qp($m, NULL, $this->options)->is($selector) > 0) {
  2987. $found->attach($m);
  2988. }
  2989. }
  2990. else {
  2991. $found->attach($m);
  2992. }
  2993. }
  2994. }
  2995. }
  2996. $this->setMatches($found);
  2997. return $this;
  2998. }
  2999. /**
  3000. * Get the next sibling before each element in the QueryPath.
  3001. *
  3002. * For each element in the QueryPath, this retrieves the previous sibling
  3003. * (if any). If a selector is supplied, it retrieves the first matching
  3004. * sibling (if any is found).
  3005. *
  3006. * @param string $selector
  3007. * A valid CSS 3 selector.
  3008. * @return QueryPath
  3009. * A QueryPath object, now containing any previous siblings that have been
  3010. * found.
  3011. * @see prevAll()
  3012. * @see next()
  3013. * @see siblings()
  3014. * @see children()
  3015. */
  3016. public function prev($selector = NULL) {
  3017. $found = new SplObjectStorage();
  3018. foreach ($this->matches as $m) {
  3019. while (isset($m->previousSibling)) {
  3020. $m = $m->previousSibling;
  3021. if ($m->nodeType === XML_ELEMENT_NODE) {
  3022. if (!empty($selector)) {
  3023. if (qp($m, NULL, $this->options)->is($selector)) {
  3024. $found->attach($m);
  3025. break;
  3026. }
  3027. }
  3028. else {
  3029. $found->attach($m);
  3030. break;
  3031. }
  3032. }
  3033. }
  3034. }
  3035. $this->setMatches($found);
  3036. return $this;
  3037. }
  3038. /**
  3039. * Get the previous siblings for each element in the QueryPath.
  3040. *
  3041. * For each element in the QueryPath, get all previous siblings. If a
  3042. * selector is provided, only matching siblings will be retrieved.
  3043. *
  3044. * @param string $selector
  3045. * A valid CSS 3 selector.
  3046. * @return QueryPath
  3047. * The QueryPath object, now wrapping previous sibling elements.
  3048. * @see prev()
  3049. * @see nextAll()
  3050. * @see siblings()
  3051. * @see contents()
  3052. * @see children()
  3053. */
  3054. public function prevAll($selector = NULL) {
  3055. $found = new SplObjectStorage();
  3056. foreach ($this->matches as $m) {
  3057. while (isset($m->previousSibling)) {
  3058. $m = $m->previousSibling;
  3059. if ($m->nodeType === XML_ELEMENT_NODE) {
  3060. if (!empty($selector)) {
  3061. if (qp($m, NULL, $this->options)->is($selector)) {
  3062. $found->attach($m);
  3063. }
  3064. }
  3065. else {
  3066. $found->attach($m);
  3067. }
  3068. }
  3069. }
  3070. }
  3071. $this->setMatches($found);
  3072. return $this;
  3073. }
  3074. /**
  3075. * @deprecated Use {@link siblings()}.
  3076. */
  3077. public function peers($selector = NULL) {
  3078. $found = new SplObjectStorage();
  3079. foreach ($this->matches as $m) {
  3080. foreach ($m->parentNode->childNodes as $kid) {
  3081. if ($kid->nodeType == XML_ELEMENT_NODE && $m !== $kid) {
  3082. if (!empty($selector)) {
  3083. if (qp($kid, NULL, $this->options)->is($selector)) {
  3084. $found->attach($kid);
  3085. }
  3086. }
  3087. else {
  3088. $found->attach($kid);
  3089. }
  3090. }
  3091. }
  3092. }
  3093. $this->setMatches($found);
  3094. return $this;
  3095. }
  3096. /**
  3097. * Add a class to all elements in the current QueryPath.
  3098. *
  3099. * This searchers for a class attribute on each item wrapped by the current
  3100. * QueryPath object. If no attribute is found, a new one is added and its value
  3101. * is set to $class. If a class attribute is found, then the value is appended
  3102. * on to the end.
  3103. *
  3104. * @param string $class
  3105. * The name of the class.
  3106. * @return QueryPath
  3107. * Returns the QueryPath object.
  3108. * @see css()
  3109. * @see attr()
  3110. * @see removeClass()
  3111. * @see hasClass()
  3112. */
  3113. public function addClass($class) {
  3114. foreach ($this->matches as $m) {
  3115. if ($m->hasAttribute('class')) {
  3116. $val = $m->getAttribute('class');
  3117. $m->setAttribute('class', $val . ' ' . $class);
  3118. }
  3119. else {
  3120. $m->setAttribute('class', $class);
  3121. }
  3122. }
  3123. return $this;
  3124. }
  3125. /**
  3126. * Remove the named class from any element in the QueryPath that has it.
  3127. *
  3128. * This may result in the entire class attribute being removed. If there
  3129. * are other items in the class attribute, though, they will not be removed.
  3130. *
  3131. * Example:
  3132. * Consider this XML:
  3133. * @code
  3134. * <element class="first second"/>
  3135. * @endcode
  3136. *
  3137. * Executing this fragment of code will remove only the 'first' class:
  3138. * @code
  3139. * qp(document, 'element')->removeClass('first');
  3140. * @endcode
  3141. *
  3142. * The resulting XML will be:
  3143. * @code
  3144. * <element class="second"/>
  3145. * @endcode
  3146. *
  3147. * To remove the entire 'class' attribute, you should use {@see removeAttr()}.
  3148. *
  3149. * @param string $class
  3150. * The class name to remove.
  3151. * @return QueryPath
  3152. * The modified QueryPath object.
  3153. * @see attr()
  3154. * @see addClass()
  3155. * @see hasClass()
  3156. */
  3157. public function removeClass($class) {
  3158. foreach ($this->matches as $m) {
  3159. if ($m->hasAttribute('class')) {
  3160. $vals = explode(' ', $m->getAttribute('class'));
  3161. if (in_array($class, $vals)) {
  3162. $buf = array();
  3163. foreach ($vals as $v) {
  3164. if ($v != $class) $buf[] = $v;
  3165. }
  3166. if (count($buf) == 0)
  3167. $m->removeAttribute('class');
  3168. else
  3169. $m->setAttribute('class', implode(' ', $buf));
  3170. }
  3171. }
  3172. }
  3173. return $this;
  3174. }
  3175. /**
  3176. * Returns TRUE if any of the elements in the QueryPath have the specified class.
  3177. *
  3178. * @param string $class
  3179. * The name of the class.
  3180. * @return boolean
  3181. * TRUE if the class exists in one or more of the elements, FALSE otherwise.
  3182. * @see addClass()
  3183. * @see removeClass()
  3184. */
  3185. public function hasClass($class) {
  3186. foreach ($this->matches as $m) {
  3187. if ($m->hasAttribute('class')) {
  3188. $vals = explode(' ', $m->getAttribute('class'));
  3189. if (in_array($class, $vals)) return TRUE;
  3190. }
  3191. }
  3192. return FALSE;
  3193. }
  3194. /**
  3195. * Branch the base QueryPath into another one with the same matches.
  3196. *
  3197. * This function makes a copy of the QueryPath object, but keeps the new copy
  3198. * (initially) pointed at the same matches. This object can then be queried without
  3199. * changing the original QueryPath. However, changes to the elements inside of this
  3200. * QueryPath will show up in the QueryPath from which it is branched.
  3201. *
  3202. * Compare this operation with {@link cloneAll()}. The cloneAll() call takes
  3203. * the current QueryPath object and makes a copy of all of its matches. You continue
  3204. * to operate on the same QueryPath object, but the elements inside of the QueryPath
  3205. * are copies of those before the call to cloneAll().
  3206. *
  3207. * This, on the other hand, copies <i>the QueryPath</i>, but keeps valid
  3208. * references to the document and the wrapped elements. A new query branch is
  3209. * created, but any changes will be written back to the same document.
  3210. *
  3211. * In practice, this comes in handy when you want to do multiple queries on a part
  3212. * of the document, but then return to a previous set of matches. (see {@link QPTPL}
  3213. * for examples of this in practice).
  3214. *
  3215. * Example:
  3216. *
  3217. * @code
  3218. * <?php
  3219. * $qp = qp(QueryPath::HTML_STUB);
  3220. * $branch = $qp->branch();
  3221. * $branch->find('title')->text('Title');
  3222. * $qp->find('body')->text('This is the body')->writeHTML;
  3223. * ?>
  3224. * @endcode
  3225. *
  3226. * Notice that in the code, each of the QueryPath objects is doing its own
  3227. * query. However, both are modifying the same document. The result of the above
  3228. * would look something like this:
  3229. *
  3230. * @code
  3231. * <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  3232. * <html xmlns="http://www.w3.org/1999/xhtml">
  3233. * <head>
  3234. * <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
  3235. * <title>Title</title>
  3236. * </head>
  3237. * <body>This is the body</body>
  3238. * </html>
  3239. * @endcode
  3240. *
  3241. * Notice that while $qp and $banch were performing separate queries, they
  3242. * both modified the same document.
  3243. *
  3244. * In jQuery or a browser-based solution, you generally do not need a branching
  3245. * function because there is (implicitly) only one document. In QueryPath, there
  3246. * is no implicit document. Every document must be explicitly specified (and,
  3247. * in most cases, parsed -- which is costly). Branching makes it possible to
  3248. * work on one document with multiple QueryPath objects.
  3249. *
  3250. * @param string $selector
  3251. * If a selector is passed in, an additional {@link find()} will be executed
  3252. * on the branch before it is returned. (Added in QueryPath 2.0.)
  3253. * @return QueryPath
  3254. * A copy of the QueryPath object that points to the same set of elements that
  3255. * the original QueryPath was pointing to.
  3256. * @since 1.1
  3257. * @see cloneAll()
  3258. * @see find()
  3259. */
  3260. public function branch($selector = NULL) {
  3261. $temp = qp($this->matches, NULL, $this->options);
  3262. if (isset($selector)) $temp->find($selector);
  3263. return $temp;
  3264. }
  3265. /**
  3266. * Perform a deep clone of each node in the QueryPath.
  3267. *
  3268. * This does not clone the QueryPath object, but instead clones the
  3269. * list of nodes wrapped by the QueryPath. Every element is deeply
  3270. * cloned.
  3271. *
  3272. * This method is analogous to jQuery's clone() method.
  3273. *
  3274. * This is a destructive operation, which means that end() will revert
  3275. * the list back to the clone's original.
  3276. * @see qp()
  3277. * @return QueryPath
  3278. */
  3279. public function cloneAll() {
  3280. $found = new SplObjectStorage();
  3281. foreach ($this->matches as $m) $found->attach($m->cloneNode(TRUE));
  3282. $this->setMatches($found, FALSE);
  3283. return $this;
  3284. }
  3285. /**
  3286. * Clone the QueryPath.
  3287. *
  3288. * This makes a deep clone of the elements inside of the QueryPath.
  3289. *
  3290. * This clones only the QueryPathImpl, not all of the decorators. The
  3291. * clone operator in PHP should handle the cloning of the decorators.
  3292. */
  3293. public function __clone() {
  3294. //XXX: Should we clone the document?
  3295. // Make sure we clone the kids.
  3296. $this->cloneAll();
  3297. }
  3298. /**
  3299. * Detach any items from the list if they match the selector.
  3300. *
  3301. * In other words, each item that matches the selector will be remove
  3302. * from the DOM document. The returned QueryPath wraps the list of
  3303. * removed elements.
  3304. *
  3305. * If no selector is specified, this will remove all current matches from
  3306. * the document.
  3307. *
  3308. * @param string $selector
  3309. * A CSS Selector.
  3310. * @return QueryPath
  3311. * The Query path wrapping a list of removed items.
  3312. * @see replaceAll()
  3313. * @see replaceWith()
  3314. * @see removeChildren()
  3315. * @since 2.1
  3316. * @author eabrand
  3317. */
  3318. public function detach($selector = NULL) {
  3319. if(!empty($selector))
  3320. $this->find($selector);
  3321. $found = new SplObjectStorage();
  3322. $this->last = $this->matches;
  3323. foreach ($this->matches as $item) {
  3324. // The item returned is (according to docs) different from
  3325. // the one passed in, so we have to re-store it.
  3326. $found->attach($item->parentNode->removeChild($item));
  3327. }
  3328. $this->setMatches($found);
  3329. return $this;
  3330. }
  3331. /**
  3332. * Attach any items from the list if they match the selector.
  3333. *
  3334. * If no selector is specified, this will remove all current matches from
  3335. * the document.
  3336. *
  3337. * @param QueryPath $dest
  3338. * A QueryPath Selector.
  3339. * @return QueryPath
  3340. * The Query path wrapping a list of removed items.
  3341. * @see replaceAll()
  3342. * @see replaceWith()
  3343. * @see removeChildren()
  3344. * @since 2.1
  3345. * @author eabrand
  3346. */
  3347. public function attach(QueryPath $dest) {
  3348. foreach ($this->last as $m) $dest->append($m);
  3349. return $this;
  3350. }
  3351. /**
  3352. * Reduce the elements matched by QueryPath to only those which contain the given item.
  3353. *
  3354. * There are two ways in which this is different from jQuery's implementation:
  3355. * - We allow ANY DOMNode, not just DOMElements. That means this will work on
  3356. * processor instructions, text nodes, comments, etc.
  3357. * - Unlike jQuery, this implementation of has() follows QueryPath standard behavior
  3358. * and modifies the existing object. It does not create a brand new object.
  3359. *
  3360. * @param mixed $contained
  3361. * - If $contained is a CSS selector (e.g. '#foo'), this will test to see
  3362. * if the current QueryPath has any elements that contain items that match
  3363. * the selector.
  3364. * - If $contained is a DOMNode, then this will test to see if THE EXACT DOMNode
  3365. * exists in the currently matched elements. (Note that you cannot match across DOM trees, even if it is the same document.)
  3366. * @since 2.1
  3367. * @author eabrand
  3368. * @todo It would be trivially easy to add support for iterating over an array or Iterable of DOMNodes.
  3369. */
  3370. public function has($contained) {
  3371. $found = new SplObjectStorage();
  3372. // If it's a selector, we just get all of the DOMNodes that match the selector.
  3373. $nodes = array();
  3374. if (is_string($contained)) {
  3375. // Get the list of nodes.
  3376. $nodes = $this->branch($contained)->get();
  3377. }
  3378. elseif ($contained instanceof DOMNode) {
  3379. // Make a list with one node.
  3380. $nodes = array($contained);
  3381. }
  3382. // Now we go through each of the nodes that we are testing. We want to find
  3383. // ALL PARENTS that are in our existing QueryPath matches. Those are the
  3384. // ones we add to our new matches.
  3385. foreach ($nodes as $original_node) {
  3386. $node = $original_node;
  3387. while (!empty($node)/* && $node != $node->ownerDocument*/) {
  3388. if ($this->matches->contains($node)) {
  3389. $found->attach($node);
  3390. }
  3391. $node = $node->parentNode;
  3392. }
  3393. }
  3394. $this->setMatches($found);
  3395. return $this;
  3396. }
  3397. /**
  3398. * Empty everything within the specified element.
  3399. *
  3400. * A convenience function for removeChildren(). This is equivalent to jQuery's
  3401. * empty() function. However, `empty` is a built-in in PHP, and cannot be used as a
  3402. * function name.
  3403. *
  3404. * @return QueryPath
  3405. * The QueryPath object with the newly emptied elements.
  3406. * @see removeChildren()
  3407. * @since 2.1
  3408. * @author eabrand
  3409. * @deprecated The removeChildren() function is the preferred method.
  3410. */
  3411. public function emptyElement() {
  3412. $this->removeChildren();
  3413. return $this;
  3414. }
  3415. /**
  3416. * Get the even elements, so counter-intuitively 1, 3, 5, etc.
  3417. *
  3418. *
  3419. *
  3420. * @return QueryPath
  3421. * A QueryPath wrapping all of the children.
  3422. * @see removeChildren()
  3423. * @see parent()
  3424. * @see parents()
  3425. * @see next()
  3426. * @see prev()
  3427. * @since 2.1
  3428. * @author eabrand
  3429. */
  3430. public function even() {
  3431. $found = new SplObjectStorage();
  3432. $even = false;
  3433. foreach ($this->matches as $m) {
  3434. if ($even && $m->nodeType == XML_ELEMENT_NODE) $found->attach($m);
  3435. $even = ($even) ? false : true;
  3436. }
  3437. $this->setMatches($found);
  3438. $this->matches = $found; // Don't buffer this. It is temporary.
  3439. return $this;
  3440. }
  3441. /**
  3442. * Get the odd elements, so counter-intuitively 0, 2, 4, etc.
  3443. *
  3444. *
  3445. *
  3446. * @return QueryPath
  3447. * A QueryPath wrapping all of the children.
  3448. * @see removeChildren()
  3449. * @see parent()
  3450. * @see parents()
  3451. * @see next()
  3452. * @see prev()
  3453. * @since 2.1
  3454. * @author eabrand
  3455. */
  3456. public function odd() {
  3457. $found = new SplObjectStorage();
  3458. $odd = true;
  3459. foreach ($this->matches as $m) {
  3460. if ($odd && $m->nodeType == XML_ELEMENT_NODE) $found->attach($m);
  3461. $odd = ($odd) ? false : true;
  3462. }
  3463. $this->setMatches($found);
  3464. $this->matches = $found; // Don't buffer this. It is temporary.
  3465. return $this;
  3466. }
  3467. /**
  3468. * Get the first matching element.
  3469. *
  3470. *
  3471. * @return QueryPath
  3472. * A QueryPath wrapping all of the children.
  3473. * @see next()
  3474. * @see prev()
  3475. * @since 2.1
  3476. * @author eabrand
  3477. */
  3478. public function first() {
  3479. $found = new SplObjectStorage();
  3480. foreach ($this->matches as $m) {
  3481. if ($m->nodeType == XML_ELEMENT_NODE) {
  3482. $found->attach($m);
  3483. break;
  3484. }
  3485. }
  3486. $this->setMatches($found);
  3487. $this->matches = $found; // Don't buffer this. It is temporary.
  3488. return $this;
  3489. }
  3490. /**
  3491. * Get the first child of the matching element.
  3492. *
  3493. *
  3494. * @return QueryPath
  3495. * A QueryPath wrapping all of the children.
  3496. * @see next()
  3497. * @see prev()
  3498. * @since 2.1
  3499. * @author eabrand
  3500. */
  3501. public function firstChild() {
  3502. // Could possibly use $m->firstChild http://theserverpages.com/php/manual/en/ref.dom.php
  3503. $found = new SplObjectStorage();
  3504. $flag = false;
  3505. foreach ($this->matches as $m) {
  3506. foreach($m->childNodes as $c) {
  3507. if ($c->nodeType == XML_ELEMENT_NODE) {
  3508. $found->attach($c);
  3509. $flag = true;
  3510. break;
  3511. }
  3512. }
  3513. if($flag) break;
  3514. }
  3515. $this->setMatches($found);
  3516. $this->matches = $found; // Don't buffer this. It is temporary.
  3517. return $this;
  3518. }
  3519. /**
  3520. * Get the last matching element.
  3521. *
  3522. *
  3523. * @return QueryPath
  3524. * A QueryPath wrapping all of the children.
  3525. * @see next()
  3526. * @see prev()
  3527. * @since 2.1
  3528. * @author eabrand
  3529. */
  3530. public function last() {
  3531. $found = new SplObjectStorage();
  3532. $item = null;
  3533. foreach ($this->matches as $m) {
  3534. if ($m->nodeType == XML_ELEMENT_NODE) {
  3535. $item = $m;
  3536. }
  3537. }
  3538. if ($item) {
  3539. $found->attach($item);
  3540. }
  3541. $this->setMatches($found);
  3542. $this->matches = $found; // Don't buffer this. It is temporary.
  3543. return $this;
  3544. }
  3545. /**
  3546. * Get the last child of the matching element.
  3547. *
  3548. *
  3549. * @return QueryPath
  3550. * A QueryPath wrapping all of the children.
  3551. * @see next()
  3552. * @see prev()
  3553. * @since 2.1
  3554. * @author eabrand
  3555. */
  3556. public function lastChild() {
  3557. $found = new SplObjectStorage();
  3558. $item = null;
  3559. foreach ($this->matches as $m) {
  3560. foreach($m->childNodes as $c) {
  3561. if ($c->nodeType == XML_ELEMENT_NODE) {
  3562. $item = $c;
  3563. }
  3564. }
  3565. if ($item) {
  3566. $found->attach($item);
  3567. $item = null;
  3568. }
  3569. }
  3570. $this->setMatches($found);
  3571. $this->matches = $found; // Don't buffer this. It is temporary.
  3572. return $this;
  3573. }
  3574. /**
  3575. * Get all siblings after an element until the selector is reached.
  3576. *
  3577. * For each element in the QueryPath, get all siblings that appear after
  3578. * it. If a selector is passed in, then only siblings that match the
  3579. * selector will be included.
  3580. *
  3581. * @param string $selector
  3582. * A valid CSS 3 selector.
  3583. * @return QueryPath
  3584. * The QueryPath object, now containing the matching siblings.
  3585. * @see next()
  3586. * @see prevAll()
  3587. * @see children()
  3588. * @see siblings()
  3589. * @since 2.1
  3590. * @author eabrand
  3591. */
  3592. public function nextUntil($selector = NULL) {
  3593. $found = new SplObjectStorage();
  3594. foreach ($this->matches as $m) {
  3595. while (isset($m->nextSibling)) {
  3596. $m = $m->nextSibling;
  3597. if ($m->nodeType === XML_ELEMENT_NODE) {
  3598. if (!empty($selector)) {
  3599. if (qp($m, NULL, $this->options)->is($selector) > 0) {
  3600. break;
  3601. }
  3602. else {
  3603. $found->attach($m);
  3604. }
  3605. }
  3606. else {
  3607. $found->attach($m);
  3608. }
  3609. }
  3610. }
  3611. }
  3612. $this->setMatches($found);
  3613. return $this;
  3614. }
  3615. /**
  3616. * Get the previous siblings for each element in the QueryPath
  3617. * until the selector is reached.
  3618. *
  3619. * For each element in the QueryPath, get all previous siblings. If a
  3620. * selector is provided, only matching siblings will be retrieved.
  3621. *
  3622. * @param string $selector
  3623. * A valid CSS 3 selector.
  3624. * @return QueryPath
  3625. * The QueryPath object, now wrapping previous sibling elements.
  3626. * @see prev()
  3627. * @see nextAll()
  3628. * @see siblings()
  3629. * @see contents()
  3630. * @see children()
  3631. * @since 2.1
  3632. * @author eabrand
  3633. */
  3634. public function prevUntil($selector = NULL) {
  3635. $found = new SplObjectStorage();
  3636. foreach ($this->matches as $m) {
  3637. while (isset($m->previousSibling)) {
  3638. $m = $m->previousSibling;
  3639. if ($m->nodeType === XML_ELEMENT_NODE) {
  3640. if (!empty($selector) && qp($m, NULL, $this->options)->is($selector))
  3641. break;
  3642. else
  3643. $found->attach($m);
  3644. }
  3645. }
  3646. }
  3647. $this->setMatches($found);
  3648. return $this;
  3649. }
  3650. /**
  3651. * Get all ancestors of each element in the QueryPath until the selector is reached.
  3652. *
  3653. * If a selector is present, only matching ancestors will be retrieved.
  3654. *
  3655. * @see parent()
  3656. * @param string $selector
  3657. * A valid CSS 3 Selector.
  3658. * @return QueryPath
  3659. * A QueryPath object containing the matching ancestors.
  3660. * @see siblings()
  3661. * @see children()
  3662. * @since 2.1
  3663. * @author eabrand
  3664. */
  3665. public function parentsUntil($selector = NULL) {
  3666. $found = new SplObjectStorage();
  3667. foreach ($this->matches as $m) {
  3668. while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
  3669. $m = $m->parentNode;
  3670. // Is there any case where parent node is not an element?
  3671. if ($m->nodeType === XML_ELEMENT_NODE) {
  3672. if (!empty($selector)) {
  3673. if (qp($m, NULL, $this->options)->is($selector) > 0)
  3674. break;
  3675. else
  3676. $found->attach($m);
  3677. }
  3678. else
  3679. $found->attach($m);
  3680. }
  3681. }
  3682. }
  3683. $this->setMatches($found);
  3684. return $this;
  3685. }
  3686. /////// INTERNAL FUNCTIONS ////////
  3687. /**
  3688. * Determine whether a given string looks like XML or not.
  3689. *
  3690. * Basically, this scans a portion of the supplied string, checking to see
  3691. * if it has a tag-like structure. It is possible to "confuse" this, which
  3692. * may subsequently result in parse errors, but in the vast majority of
  3693. * cases, this method serves as a valid inicator of whether or not the
  3694. * content looks like XML.
  3695. *
  3696. * Things that are intentional excluded:
  3697. * - plain text with no markup.
  3698. * - strings that look like filesystem paths.
  3699. *
  3700. * Subclasses SHOULD NOT OVERRIDE THIS. Altering it may be altering
  3701. * core assumptions about how things work. Instead, classes should
  3702. * override the constructor and pass in only one of the parsed types
  3703. * that this class expects.
  3704. */
  3705. protected function isXMLish($string) {
  3706. // Long strings will exhaust the regex engine, so we
  3707. // grab a representative string.
  3708. // $test = substr($string, 0, 255);
  3709. return (strpos($string, '<') !== FALSE && strpos($string, '>') !== FALSE);
  3710. //return preg_match(ML_EXP, $test) > 0;
  3711. }
  3712. private function parseXMLString($string, $flags = NULL) {
  3713. $document = new DOMDocument('1.0');
  3714. $lead = strtolower(substr($string, 0, 5)); // <?xml
  3715. try {
  3716. set_error_handler(array('QueryPathParseException', 'initializeFromError'), $this->errTypes);
  3717. if (isset($this->options['convert_to_encoding'])) {
  3718. // Is there another way to do this?
  3719. $from_enc = isset($this->options['convert_from_encoding']) ? $this->options['convert_from_encoding'] : 'auto';
  3720. $to_enc = $this->options['convert_to_encoding'];
  3721. if (function_exists('mb_convert_encoding')) {
  3722. $string = mb_convert_encoding($string, $to_enc, $from_enc);
  3723. }
  3724. }
  3725. // This is to avoid cases where low ascii digits have slipped into HTML.
  3726. // AFAIK, it should not adversly effect UTF-8 documents.
  3727. if (!empty($this->options['strip_low_ascii'])) {
  3728. $string = filter_var($string, FILTER_UNSAFE_RAW, FILTER_FLAG_ENCODE_LOW);
  3729. }
  3730. // Allow users to override parser settings.
  3731. if (empty($this->options['use_parser'])) {
  3732. $useParser = '';
  3733. }
  3734. else {
  3735. $useParser = strtolower($this->options['use_parser']);
  3736. }
  3737. // If HTML parser is requested, we use it.
  3738. if ($useParser == 'html') {
  3739. $document->loadHTML($string);
  3740. }
  3741. // Parse as XML if it looks like XML, or if XML parser is requested.
  3742. elseif ($lead == '<?xml' || $useParser == 'xml') {
  3743. if ($this->options['replace_entities']) {
  3744. $string = QueryPathEntities::replaceAllEntities($string);
  3745. }
  3746. $document->loadXML($string, $flags);
  3747. }
  3748. // In all other cases, we try the HTML parser.
  3749. else {
  3750. $document->loadHTML($string);
  3751. }
  3752. }
  3753. // Emulate 'finally' behavior.
  3754. catch (Exception $e) {
  3755. restore_error_handler();
  3756. throw $e;
  3757. }
  3758. restore_error_handler();
  3759. if (empty($document)) {
  3760. throw new QueryPathParseException('Unknown parser exception.');
  3761. }
  3762. return $document;
  3763. }
  3764. /**
  3765. * EXPERT: Be very, very careful using this.
  3766. * A utility function for setting the current set of matches.
  3767. * It makes sure the last matches buffer is set (for end() and andSelf()).
  3768. * @since 2.0
  3769. */
  3770. public function setMatches($matches, $unique = TRUE) {
  3771. // This causes a lot of overhead....
  3772. //if ($unique) $matches = self::unique($matches);
  3773. $this->last = $this->matches;
  3774. // Just set current matches.
  3775. if ($matches instanceof SplObjectStorage) {
  3776. $this->matches = $matches;
  3777. }
  3778. // This is likely legacy code that needs conversion.
  3779. elseif (is_array($matches)) {
  3780. trigger_error('Legacy array detected.');
  3781. $tmp = new SplObjectStorage();
  3782. foreach ($matches as $m) $tmp->attach($m);
  3783. $this->matches = $tmp;
  3784. }
  3785. // For non-arrays, try to create a new match set and
  3786. // add this object.
  3787. else {
  3788. $found = new SplObjectStorage();
  3789. if (isset($matches)) $found->attach($matches);
  3790. $this->matches = $found;
  3791. }
  3792. // EXPERIMENTAL: Support for qp()->length.
  3793. $this->length = $this->matches->count();
  3794. }
  3795. /**
  3796. * Set the match monitor to empty.
  3797. *
  3798. * This preserves history.
  3799. *
  3800. * @since 2.0
  3801. */
  3802. private function noMatches() {
  3803. $this->setMatches(NULL);
  3804. }
  3805. /**
  3806. * A utility function for retriving a match by index.
  3807. *
  3808. * The internal data structure used in QueryPath does not have
  3809. * strong random access support, so we suppliment it with this method.
  3810. */
  3811. private function getNthMatch($index) {
  3812. if ($index > $this->matches->count() || $index < 0) return;
  3813. $i = 0;
  3814. foreach ($this->matches as $m) {
  3815. if ($i++ == $index) return $m;
  3816. }
  3817. }
  3818. /**
  3819. * Convenience function for getNthMatch(0).
  3820. */
  3821. private function getFirstMatch() {
  3822. $this->matches->rewind();
  3823. return $this->matches->current();
  3824. }
  3825. /**
  3826. * Parse just a fragment of XML.
  3827. * This will automatically prepend an <?xml ?> declaration before parsing.
  3828. * @param string $string
  3829. * Fragment to parse.
  3830. * @return DOMDocumentFragment
  3831. * The parsed document fragment.
  3832. */
  3833. /*
  3834. private function parseXMLFragment($string) {
  3835. $frag = $this->document->createDocumentFragment();
  3836. $frag->appendXML($string);
  3837. return $frag;
  3838. }
  3839. */
  3840. /**
  3841. * Parse an XML or HTML file.
  3842. *
  3843. * This attempts to autodetect the type of file, and then parse it.
  3844. *
  3845. * @param string $filename
  3846. * The file name to parse.
  3847. * @param int $flags
  3848. * The OR-combined flags accepted by the DOM parser. See the PHP documentation
  3849. * for DOM or for libxml.
  3850. * @param resource $context
  3851. * The stream context for the file IO. If this is set, then an alternate
  3852. * parsing path is followed: The file is loaded by PHP's stream-aware IO
  3853. * facilities, read entirely into memory, and then handed off to
  3854. * {@link parseXMLString()}. On large files, this can have a performance impact.
  3855. * @throws QueryPathParseException
  3856. * Thrown when a file cannot be loaded or parsed.
  3857. */
  3858. private function parseXMLFile($filename, $flags = NULL, $context = NULL) {
  3859. // If a context is specified, we basically have to do the reading in
  3860. // two steps:
  3861. if (!empty($context)) {
  3862. try {
  3863. set_error_handler(array('QueryPathParseException', 'initializeFromError'), $this->errTypes);
  3864. $contents = file_get_contents($filename, FALSE, $context);
  3865. }
  3866. // Apparently there is no 'finally' in PHP, so we have to restore the error
  3867. // handler this way:
  3868. catch(Exception $e) {
  3869. restore_error_handler();
  3870. throw $e;
  3871. }
  3872. restore_error_handler();
  3873. if ($contents == FALSE) {
  3874. throw new QueryPathParseException(sprintf('Contents of the file %s could not be retrieved.', $filename));
  3875. }
  3876. /* This is basically unneccessary overhead, as it is not more
  3877. * accurate than the existing method.
  3878. if (isset($md['wrapper_type']) && $md['wrapper_type'] == 'http') {
  3879. for ($i = 0; $i < count($md['wrapper_data']); ++$i) {
  3880. if (stripos($md['wrapper_data'][$i], 'content-type:') !== FALSE) {
  3881. $ct = trim(substr($md['wrapper_data'][$i], 12));
  3882. if (stripos('text/html') === 0) {
  3883. $this->parseXMLString($contents, $flags, 'text/html');
  3884. }
  3885. else {
  3886. // We can't account for all of the mime types that have
  3887. // an XML payload, so we set it to XML.
  3888. $this->parseXMLString($contents, $flags, 'text/xml');
  3889. }
  3890. break;
  3891. }
  3892. }
  3893. }
  3894. */
  3895. return $this->parseXMLString($contents, $flags);
  3896. }
  3897. $document = new DOMDocument();
  3898. $lastDot = strrpos($filename, '.');
  3899. $htmlExtensions = array(
  3900. '.html' => 1,
  3901. '.htm' => 1,
  3902. );
  3903. // Allow users to override parser settings.
  3904. if (empty($this->options['use_parser'])) {
  3905. $useParser = '';
  3906. }
  3907. else {
  3908. $useParser = strtolower($this->options['use_parser']);
  3909. }
  3910. $ext = $lastDot !== FALSE ? strtolower(substr($filename, $lastDot)) : '';
  3911. try {
  3912. set_error_handler(array('QueryPathParseException', 'initializeFromError'), $this->errTypes);
  3913. // If the parser is explicitly set to XML, use that parser.
  3914. if ($useParser == 'xml') {
  3915. $r = $document->load($filename, $flags);
  3916. }
  3917. // Otherwise, see if it looks like HTML.
  3918. elseif (isset($htmlExtensions[$ext]) || $useParser == 'html') {
  3919. // Try parsing it as HTML.
  3920. $r = $document->loadHTMLFile($filename);
  3921. }
  3922. // Default to XML.
  3923. else {
  3924. $r = $document->load($filename, $flags);
  3925. }
  3926. }
  3927. // Emulate 'finally' behavior.
  3928. catch (Exception $e) {
  3929. restore_error_handler();
  3930. throw $e;
  3931. }
  3932. restore_error_handler();
  3933. /*
  3934. if ($r == FALSE) {
  3935. $fmt = 'Failed to load file %s: %s (%s, %s)';
  3936. $err = error_get_last();
  3937. if ($err['type'] & self::IGNORE_ERRORS) {
  3938. // Need to report these somehow...
  3939. trigger_error($err['message'], E_USER_WARNING);
  3940. }
  3941. else {
  3942. throw new QueryPathParseException(sprintf($fmt, $filename, $err['message'], $err['file'], $err['line']));
  3943. }
  3944. //throw new QueryPathParseException(sprintf($fmt, $filename, $err['message'], $err['file'], $err['line']));
  3945. }
  3946. */
  3947. return $document;
  3948. }
  3949. /**
  3950. * Call extension methods.
  3951. *
  3952. * This function is used to invoke extension methods. It searches the
  3953. * registered extenstensions for a matching function name. If one is found,
  3954. * it is executed with the arguments in the $arguments array.
  3955. *
  3956. * @throws QueryPathException
  3957. * An exception is thrown if a non-existent method is called.
  3958. */
  3959. public function __call($name, $arguments) {
  3960. if (!QueryPathExtensionRegistry::$useRegistry) {
  3961. throw new QueryPathException("No method named $name found (Extensions disabled).");
  3962. }
  3963. // Loading of extensions is deferred until the first time a
  3964. // non-core method is called. This makes constructing faster, but it
  3965. // may make the first invocation of __call() slower (if there are
  3966. // enough extensions.)
  3967. //
  3968. // The main reason for moving this out of the constructor is that most
  3969. // new QueryPath instances do not use extensions. Charging qp() calls
  3970. // with the additional hit is not a good idea.
  3971. //
  3972. // Also, this will at least limit the number of circular references.
  3973. if (empty($this->ext)) {
  3974. // Load the registry
  3975. $this->ext = QueryPathExtensionRegistry::getExtensions($this);
  3976. }
  3977. // Note that an empty ext registry indicates that extensions are disabled.
  3978. if (!empty($this->ext) && QueryPathExtensionRegistry::hasMethod($name)) {
  3979. $owner = QueryPathExtensionRegistry::getMethodClass($name);
  3980. $method = new ReflectionMethod($owner, $name);
  3981. return $method->invokeArgs($this->ext[$owner], $arguments);
  3982. }
  3983. throw new QueryPathException("No method named $name found. Possibly missing an extension.");
  3984. }
  3985. /**
  3986. * Dynamically generate certain properties.
  3987. *
  3988. * This is used primarily to increase jQuery compatibility by providing property-like
  3989. * behaviors.
  3990. *
  3991. * Currently defined properties:
  3992. * - length: Alias of {@link size()}.
  3993. */
  3994. /*
  3995. public function __get($name) {
  3996. switch ($name) {
  3997. case 'length':
  3998. return $this->size();
  3999. default:
  4000. throw new QueryPathException('Unknown or inaccessible property "' . $name . '" (via __get())');
  4001. }
  4002. }
  4003. */
  4004. /**
  4005. * Get an iterator for the matches in this object.
  4006. * @return Iterable
  4007. * Returns an iterator.
  4008. */
  4009. public function getIterator() {
  4010. $i = new QueryPathIterator($this->matches);
  4011. $i->options = $this->options;
  4012. return $i;
  4013. }
  4014. }
  4015. /**
  4016. * Perform various tasks on HTML/XML entities.
  4017. *
  4018. * @ingroup querypath_util
  4019. */
  4020. class QueryPathEntities {
  4021. /**
  4022. * This is three regexes wrapped into 1. The | divides them.
  4023. * 1: Match any char-based entity. This will go in $matches[1]
  4024. * 2: Match any num-based entity. This will go in $matches[2]
  4025. * 3: Match any hex-based entry. This will go in $matches[3]
  4026. * 4: Match any ampersand that is not an entity. This goes in $matches[4]
  4027. * This last rule will only match if one of the previous two has not already
  4028. * matched.
  4029. * XXX: Are octal encodings for entities acceptable?
  4030. */
  4031. //protected static $regex = '/&([\w]+);|&#([\d]+);|&([\w]*[\s$]+)/m';
  4032. protected static $regex = '/&([\w]+);|&#([\d]+);|&#(x[0-9a-fA-F]+);|(&)/m';
  4033. /**
  4034. * Replace all entities.
  4035. * This will scan a string and will attempt to replace all
  4036. * entities with their numeric equivalent. This will not work
  4037. * with specialized entities.
  4038. *
  4039. * @param string $string
  4040. * The string to perform replacements on.
  4041. * @return string
  4042. * Returns a string that is similar to the original one, but with
  4043. * all entity replacements made.
  4044. */
  4045. public static function replaceAllEntities($string) {
  4046. return preg_replace_callback(self::$regex, 'QueryPathEntities::doReplacement', $string);
  4047. }
  4048. /**
  4049. * Callback for processing replacements.
  4050. *
  4051. * @param array $matches
  4052. * The regular expression replacement array.
  4053. */
  4054. protected static function doReplacement($matches) {
  4055. // See how the regex above works out.
  4056. //print_r($matches);
  4057. // From count, we can tell whether we got a
  4058. // char, num, or bare ampersand.
  4059. $count = count($matches);
  4060. switch ($count) {
  4061. case 2:
  4062. // We have a character entity
  4063. return '&#' . self::replaceEntity($matches[1]) . ';';
  4064. case 3:
  4065. case 4:
  4066. // we have a numeric entity
  4067. return '&#' . $matches[$count-1] . ';';
  4068. case 5:
  4069. // We have an unescaped ampersand.
  4070. return '&#38;';
  4071. }
  4072. }
  4073. /**
  4074. * Lookup an entity string's numeric equivalent.
  4075. *
  4076. * @param string $entity
  4077. * The entity whose numeric value is needed.
  4078. * @return int
  4079. * The integer value corresponding to the entity.
  4080. * @author Matt Butcher
  4081. * @author Ryan Mahoney
  4082. */
  4083. public static function replaceEntity($entity) {
  4084. return self::$entity_array[$entity];
  4085. }
  4086. /**
  4087. * Conversion mapper for entities in HTML.
  4088. * Large entity conversion table. This is
  4089. * significantly broader in range than
  4090. * get_html_translation_table(HTML_ENTITIES).
  4091. *
  4092. * This code comes from Rhizome ({@link http://code.google.com/p/sinciput})
  4093. *
  4094. * @see get_html_translation_table()
  4095. */
  4096. private static $entity_array = array(
  4097. 'nbsp' => 160, 'iexcl' => 161, 'cent' => 162, 'pound' => 163,
  4098. 'curren' => 164, 'yen' => 165, 'brvbar' => 166, 'sect' => 167,
  4099. 'uml' => 168, 'copy' => 169, 'ordf' => 170, 'laquo' => 171,
  4100. 'not' => 172, 'shy' => 173, 'reg' => 174, 'macr' => 175, 'deg' => 176,
  4101. 'plusmn' => 177, 'sup2' => 178, 'sup3' => 179, 'acute' => 180,
  4102. 'micro' => 181, 'para' => 182, 'middot' => 183, 'cedil' => 184,
  4103. 'sup1' => 185, 'ordm' => 186, 'raquo' => 187, 'frac14' => 188,
  4104. 'frac12' => 189, 'frac34' => 190, 'iquest' => 191, 'Agrave' => 192,
  4105. 'Aacute' => 193, 'Acirc' => 194, 'Atilde' => 195, 'Auml' => 196,
  4106. 'Aring' => 197, 'AElig' => 198, 'Ccedil' => 199, 'Egrave' => 200,
  4107. 'Eacute' => 201, 'Ecirc' => 202, 'Euml' => 203, 'Igrave' => 204,
  4108. 'Iacute' => 205, 'Icirc' => 206, 'Iuml' => 207, 'ETH' => 208,
  4109. 'Ntilde' => 209, 'Ograve' => 210, 'Oacute' => 211, 'Ocirc' => 212,
  4110. 'Otilde' => 213, 'Ouml' => 214, 'times' => 215, 'Oslash' => 216,
  4111. 'Ugrave' => 217, 'Uacute' => 218, 'Ucirc' => 219, 'Uuml' => 220,
  4112. 'Yacute' => 221, 'THORN' => 222, 'szlig' => 223, 'agrave' => 224,
  4113. 'aacute' => 225, 'acirc' => 226, 'atilde' => 227, 'auml' => 228,
  4114. 'aring' => 229, 'aelig' => 230, 'ccedil' => 231, 'egrave' => 232,
  4115. 'eacute' => 233, 'ecirc' => 234, 'euml' => 235, 'igrave' => 236,
  4116. 'iacute' => 237, 'icirc' => 238, 'iuml' => 239, 'eth' => 240,
  4117. 'ntilde' => 241, 'ograve' => 242, 'oacute' => 243, 'ocirc' => 244,
  4118. 'otilde' => 245, 'ouml' => 246, 'divide' => 247, 'oslash' => 248,
  4119. 'ugrave' => 249, 'uacute' => 250, 'ucirc' => 251, 'uuml' => 252,
  4120. 'yacute' => 253, 'thorn' => 254, 'yuml' => 255, 'quot' => 34,
  4121. 'amp' => 38, 'lt' => 60, 'gt' => 62, 'apos' => 39, 'OElig' => 338,
  4122. 'oelig' => 339, 'Scaron' => 352, 'scaron' => 353, 'Yuml' => 376,
  4123. 'circ' => 710, 'tilde' => 732, 'ensp' => 8194, 'emsp' => 8195,
  4124. 'thinsp' => 8201, 'zwnj' => 8204, 'zwj' => 8205, 'lrm' => 8206,
  4125. 'rlm' => 8207, 'ndash' => 8211, 'mdash' => 8212, 'lsquo' => 8216,
  4126. 'rsquo' => 8217, 'sbquo' => 8218, 'ldquo' => 8220, 'rdquo' => 8221,
  4127. 'bdquo' => 8222, 'dagger' => 8224, 'Dagger' => 8225, 'permil' => 8240,
  4128. 'lsaquo' => 8249, 'rsaquo' => 8250, 'euro' => 8364, 'fnof' => 402,
  4129. 'Alpha' => 913, 'Beta' => 914, 'Gamma' => 915, 'Delta' => 916,
  4130. 'Epsilon' => 917, 'Zeta' => 918, 'Eta' => 919, 'Theta' => 920,
  4131. 'Iota' => 921, 'Kappa' => 922, 'Lambda' => 923, 'Mu' => 924, 'Nu' => 925,
  4132. 'Xi' => 926, 'Omicron' => 927, 'Pi' => 928, 'Rho' => 929, 'Sigma' => 931,
  4133. 'Tau' => 932, 'Upsilon' => 933, 'Phi' => 934, 'Chi' => 935, 'Psi' => 936,
  4134. 'Omega' => 937, 'alpha' => 945, 'beta' => 946, 'gamma' => 947,
  4135. 'delta' => 948, 'epsilon' => 949, 'zeta' => 950, 'eta' => 951,
  4136. 'theta' => 952, 'iota' => 953, 'kappa' => 954, 'lambda' => 955,
  4137. 'mu' => 956, 'nu' => 957, 'xi' => 958, 'omicron' => 959, 'pi' => 960,
  4138. 'rho' => 961, 'sigmaf' => 962, 'sigma' => 963, 'tau' => 964,
  4139. 'upsilon' => 965, 'phi' => 966, 'chi' => 967, 'psi' => 968,
  4140. 'omega' => 969, 'thetasym' => 977, 'upsih' => 978, 'piv' => 982,
  4141. 'bull' => 8226, 'hellip' => 8230, 'prime' => 8242, 'Prime' => 8243,
  4142. 'oline' => 8254, 'frasl' => 8260, 'weierp' => 8472, 'image' => 8465,
  4143. 'real' => 8476, 'trade' => 8482, 'alefsym' => 8501, 'larr' => 8592,
  4144. 'uarr' => 8593, 'rarr' => 8594, 'darr' => 8595, 'harr' => 8596,
  4145. 'crarr' => 8629, 'lArr' => 8656, 'uArr' => 8657, 'rArr' => 8658,
  4146. 'dArr' => 8659, 'hArr' => 8660, 'forall' => 8704, 'part' => 8706,
  4147. 'exist' => 8707, 'empty' => 8709, 'nabla' => 8711, 'isin' => 8712,
  4148. 'notin' => 8713, 'ni' => 8715, 'prod' => 8719, 'sum' => 8721,
  4149. 'minus' => 8722, 'lowast' => 8727, 'radic' => 8730, 'prop' => 8733,
  4150. 'infin' => 8734, 'ang' => 8736, 'and' => 8743, 'or' => 8744, 'cap' => 8745,
  4151. 'cup' => 8746, 'int' => 8747, 'there4' => 8756, 'sim' => 8764,
  4152. 'cong' => 8773, 'asymp' => 8776, 'ne' => 8800, 'equiv' => 8801,
  4153. 'le' => 8804, 'ge' => 8805, 'sub' => 8834, 'sup' => 8835, 'nsub' => 8836,
  4154. 'sube' => 8838, 'supe' => 8839, 'oplus' => 8853, 'otimes' => 8855,
  4155. 'perp' => 8869, 'sdot' => 8901, 'lceil' => 8968, 'rceil' => 8969,
  4156. 'lfloor' => 8970, 'rfloor' => 8971, 'lang' => 9001, 'rang' => 9002,
  4157. 'loz' => 9674, 'spades' => 9824, 'clubs' => 9827, 'hearts' => 9829,
  4158. 'diams' => 9830
  4159. );
  4160. }
  4161. /**
  4162. * An iterator for QueryPath.
  4163. *
  4164. * This provides iterator support for QueryPath. You do not need to construct
  4165. * a QueryPathIterator. QueryPath does this when its {@link QueryPath::getIterator()}
  4166. * method is called.
  4167. *
  4168. * @ingroup querypath_util
  4169. */
  4170. class QueryPathIterator extends IteratorIterator {
  4171. public $options = array();
  4172. private $qp = NULL;
  4173. public function current() {
  4174. if (!isset($this->qp)) {
  4175. $this->qp = qp(parent::current(), NULL, $this->options);
  4176. }
  4177. else {
  4178. $splos = new SplObjectStorage();
  4179. $splos->attach(parent::current());
  4180. $this->qp->setMatches($splos);
  4181. }
  4182. return $this->qp;
  4183. }
  4184. }
  4185. /**
  4186. * Manage default options.
  4187. *
  4188. * This class stores the default options for QueryPath. When a new
  4189. * QueryPath object is constructed, options specified here will be
  4190. * used.
  4191. *
  4192. * <b>Details</b>
  4193. * This class defines no options of its own. Instead, it provides a
  4194. * central tool for developers to override options set by QueryPath.
  4195. * When a QueryPath object is created, it will evaluate options in the
  4196. * following order:
  4197. *
  4198. * - Options passed into {@link qp()} have highest priority.
  4199. * - Options in {@link QueryPathOptions} (this class) have the next highest priority.
  4200. * - If the option is not specified elsewhere, QueryPath will use its own defaults.
  4201. *
  4202. * @see qp()
  4203. * @see QueryPathOptions::set()
  4204. * @ingroup querypath_util
  4205. */
  4206. class QueryPathOptions {
  4207. /**
  4208. * This is the static options array.
  4209. *
  4210. * Use the {@link set()}, {@link get()}, and {@link merge()} to
  4211. * modify this array.
  4212. */
  4213. static $options = array();
  4214. /**
  4215. * Set the default options.
  4216. *
  4217. * The passed-in array will be used as the default options list.
  4218. *
  4219. * @param array $array
  4220. * An associative array of options.
  4221. */
  4222. static function set($array) {
  4223. self::$options = $array;
  4224. }
  4225. /**
  4226. * Get the default options.
  4227. *
  4228. * Get all options currently set as default.
  4229. *
  4230. * @return array
  4231. * An array of options. Note that only explicitly set options are
  4232. * returned. {@link QueryPath} defines default options which are not
  4233. * stored in this object.
  4234. */
  4235. static function get() {
  4236. return self::$options;
  4237. }
  4238. /**
  4239. * Merge the provided array with existing options.
  4240. *
  4241. * On duplicate keys, the value in $array will overwrite the
  4242. * value stored in the options.
  4243. *
  4244. * @param array $array
  4245. * Associative array of options to merge into the existing options.
  4246. */
  4247. static function merge($array) {
  4248. self::$options = $array + self::$options;
  4249. }
  4250. /**
  4251. * Returns true of the specified key is already overridden in this object.
  4252. *
  4253. * @param string $key
  4254. * The key to search for.
  4255. */
  4256. static function has($key) {
  4257. return array_key_exists($key, self::$options);
  4258. }
  4259. }
  4260. /**
  4261. * Exception indicating that a problem has occured inside of a QueryPath object.
  4262. *
  4263. * @ingroup querypath_core
  4264. */
  4265. class QueryPathException extends Exception {}
  4266. /**
  4267. * Exception indicating that a parser has failed to parse a file.
  4268. *
  4269. * This will report parser warnings as well as parser errors. It should only be
  4270. * thrown, though, under error conditions.
  4271. *
  4272. * @ingroup querypath_core
  4273. */
  4274. class QueryPathParseException extends QueryPathException {
  4275. const ERR_MSG_FORMAT = 'Parse error in %s on line %d column %d: %s (%d)';
  4276. const WARN_MSG_FORMAT = 'Parser warning in %s on line %d column %d: %s (%d)';
  4277. // trigger_error
  4278. public function __construct($msg = '', $code = 0, $file = NULL, $line = NULL) {
  4279. $msgs = array();
  4280. foreach(libxml_get_errors() as $err) {
  4281. $format = $err->level == LIBXML_ERR_WARNING ? self::WARN_MSG_FORMAT : self::ERR_MSG_FORMAT;
  4282. $msgs[] = sprintf($format, $err->file, $err->line, $err->column, $err->message, $err->code);
  4283. }
  4284. $msg .= implode("\n", $msgs);
  4285. if (isset($file)) {
  4286. $msg .= ' (' . $file;
  4287. if (isset($line)) $msg .= ': ' . $line;
  4288. $msg .= ')';
  4289. }
  4290. parent::__construct($msg, $code);
  4291. }
  4292. public static function initializeFromError($code, $str, $file, $line, $cxt) {
  4293. //printf("\n\nCODE: %s %s\n\n", $code, $str);
  4294. $class = __CLASS__;
  4295. throw new $class($str, $code, $file, $line);
  4296. }
  4297. }
  4298. /**
  4299. * Indicates that an input/output exception has occurred.
  4300. *
  4301. * @ingroup querypath_core
  4302. */
  4303. class QueryPathIOException extends QueryPathParseException {
  4304. public static function initializeFromError($code, $str, $file, $line, $cxt) {
  4305. $class = __CLASS__;
  4306. throw new $class($str, $code, $file, $line);
  4307. }
  4308. }