1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543 |
- <?php
- /** @file
- * The Query Path package provides tools for manipulating a Document Object Model.
- * The two major DOMs are the XML DOM and the HTML DOM. Using Query Path, you can
- * build, parse, search, and modify DOM documents.
- *
- * To use Query Path, this is the only file you should need to import.
- *
- * Standard usage:
- * @code
- * <?php
- * require 'QueryPath/QueryPath.php';
- * $qp = qp('#myID', '<?xml version="1.0"?><test><foo id="myID"/></test>');
- * $qp->append('<new><elements/></new>')->writeHTML();
- * ?>
- * @endcode
- *
- * The above would print (formatted for readability):
- * @code
- * <?xml version="1.0"?>
- * <test>
- * <foo id="myID">
- * <new>
- * <element/>
- * </new>
- * </foo>
- * </test>
- * @endcode
- *
- * To learn about the functions available to a Query Path object,
- * see {@link QueryPath}. The {@link qp()} function is used to build
- * new QueryPath objects. The documentation for that function explains the
- * wealth of arguments that the function can take.
- *
- * Included with the source code for QueryPath is a complete set of unit tests
- * as well as some example files. Those are good resources for learning about
- * how to apply QueryPath's tools. The full API documentation can be generated
- * from these files using PHPDocumentor.
- *
- * If you are interested in building extensions for QueryParser, see the
- * {@link QueryPathExtender} class. There, you will find information on adding
- * your own tools to QueryPath.
- *
- * QueryPath also comes with a full CSS 3 selector parser implementation. If
- * you are interested in reusing that in other code, you will want to start
- * with {@link CssEventHandler.php}, which is the event interface for the parser.
- *
- * All of the code in QueryPath is licensed under either the LGPL or an MIT-like
- * license (you may choose which you prefer). All of the code is Copyright, 2009
- * by Matt Butcher.
- *
- * @author M Butcher <matt @aleph-null.tv>
- * @license http://opensource.org/licenses/lgpl-2.1.php The GNU Lesser GPL (LGPL) or an MIT-like license.
- * @see QueryPath
- * @see qp()
- * @see http://querypath.org The QueryPath home page.
- * @see http://api.querypath.org An online version of the API docs.
- * @see http://technosophos.com For how-tos and examples.
- * @copyright Copyright (c) 2009, Matt Butcher.
- * @version -UNSTABLE%
- *
- */
- /** @addtogroup querypath_core Core API
- * Core classes and functions for QueryPath.
- *
- * These are the classes, objects, and functions that developers who use QueryPath
- * are likely to use. The qp() and htmlqp() functions are the best place to start,
- * while most of the frequently used methods are part of the QueryPath object.
- */
- /** @addtogroup querypath_util Utilities
- * Utility classes for QueryPath.
- *
- * These classes add important, but less-often used features to QueryPath. Some of
- * these are used transparently (QueryPathIterator). Others you can use directly in your
- * code (QueryPathEntities).
- */
- /* * @namespace QueryPath
- * The core classes that compose QueryPath.
- *
- * The QueryPath classes contain the brunt of the QueryPath code. If you are
- * interested in working with just the CSS engine, you may want to look at CssEventHandler,
- * which can be used without the rest of QueryPath. If you are interested in looking
- * carefully at QueryPath's implementation details, then the QueryPath class is where you
- * should begin. If you are interested in writing extensions, than you may want to look at
- * QueryPathExtension, and also at some of the simple extensions, such as QPXML.
- */
-
- /**
- * Regular expression for checking whether a string looks like XML.
- * @deprecated This is no longer used in QueryPath.
- */
- define('ML_EXP','/^[^<]*(<(.|\s)+>)[^>]*$/');
- /**
- * The CssEventHandler interfaces with the CSS parser.
- */
- require_once 'CssEventHandler.php';
- /**
- * The extender is used to provide support for extensions.
- */
- require_once 'QueryPathExtension.php';
- /**
- * Build a new Query Path.
- * This builds a new Query Path object. The new object can be used for
- * reading, search, and modifying a document.
- *
- * While it is permissible to directly create new instances of a QueryPath
- * implementation, it is not advised. Instead, you should use this function
- * as a factory.
- *
- * Example:
- * @code
- * <?php
- * qp(); // New empty QueryPath
- * qp('path/to/file.xml'); // From a file
- * qp('<html><head></head><body></body></html>'); // From HTML or XML
- * qp(QueryPath::XHTML_STUB); // From a basic HTML document.
- * qp(QueryPath::XHTML_STUB, 'title'); // Create one from a basic HTML doc and position it at the title element.
- *
- * // Most of the time, methods are chained directly off of this call.
- * qp(QueryPath::XHTML_STUB, 'body')->append('<h1>Title</h1>')->addClass('body-class');
- * ?>
- * @endcode
- *
- * This function is used internally by QueryPath. Anything that modifies the
- * behavior of this function may also modify the behavior of common QueryPath
- * methods.
- *
- * <b>Types of documents that QueryPath can support</b>
- *
- * qp() can take any of these as its first argument:
- *
- * - A string of XML or HTML (See {@link XHTML_STUB})
- * - A path on the file system or a URL
- * - A {@link DOMDocument} object
- * - A {@link SimpleXMLElement} object.
- * - A {@link DOMNode} object.
- * - An array of {@link DOMNode} objects (generally {@link DOMElement} nodes).
- * - Another {@link QueryPath} object.
- *
- * Keep in mind that most features of QueryPath operate on elements. Other
- * sorts of DOMNodes might not work with all features.
- *
- * <b>Supported Options</b>
- * - context: A stream context object. This is used to pass context info
- * to the underlying file IO subsystem.
- * - encoding: A valid character encoding, such as 'utf-8' or 'ISO-8859-1'.
- * The default is system-dependant, typically UTF-8. Note that this is
- * only used when creating new documents, not when reading existing content.
- * (See convert_to_encoding below.)
- * - parser_flags: An OR-combined set of parser flags. The flags supported
- * by the DOMDocument PHP class are all supported here.
- * - omit_xml_declaration: Boolean. If this is TRUE, then certain output
- * methods (like {@link QueryPath::xml()}) will omit the XML declaration
- * from the beginning of a document.
- * - replace_entities: Boolean. If this is TRUE, then any of the insertion
- * functions (before(), append(), etc.) will replace named entities with
- * their decimal equivalent, and will replace un-escaped ampersands with
- * a numeric entity equivalent.
- * - ignore_parser_warnings: Boolean. If this is TRUE, then E_WARNING messages
- * generated by the XML parser will not cause QueryPath to throw an exception.
- * This is useful when parsing
- * badly mangled HTML, or when failure to find files should not result in
- * an exception. By default, this is FALSE -- that is, parsing warnings and
- * IO warnings throw exceptions.
- * - convert_to_encoding: Use the MB library to convert the document to the
- * named encoding before parsing. This is useful for old HTML (set it to
- * iso-8859-1 for best results). If this is not supplied, no character set
- * conversion will be performed. See {@link mb_convert_encoding()}.
- * (QueryPath 1.3 and later)
- * - convert_from_encoding: If 'convert_to_encoding' is set, this option can be
- * used to explicitly define what character set the source document is using.
- * By default, QueryPath will allow the MB library to guess the encoding.
- * (QueryPath 1.3 and later)
- * - strip_low_ascii: If this is set to TRUE then markup will have all low ASCII
- * characters (<32) stripped out before parsing. This is good in cases where
- * icky HTML has (illegal) low characters in the document.
- * - use_parser: If 'xml', Parse the document as XML. If 'html', parse the
- * document as HTML. Note that the XML parser is very strict, while the
- * HTML parser is more lenient, but does enforce some of the DTD/Schema.
- * <i>By default, QueryPath autodetects the type.</i>
- * - escape_xhtml_js_css_sections: XHTML needs script and css sections to be
- * escaped. Yet older readers do not handle CDATA sections, and comments do not
- * work properly (for numerous reasons). By default, QueryPath's *XHTML methods
- * will wrap a script body with a CDATA declaration inside of C-style comments.
- * If you want to change this, you can set this option with one of the
- * JS_CSS_ESCAPE_* constants, or you can write your own.
- * - QueryPath_class: (ADVANCED) Use this to set the actual classname that
- * {@link qp()} loads as a QueryPath instance. It is assumed that the
- * class is either {@link QueryPath} or a subclass thereof. See the test
- * cases for an example.
- *
- * @ingroup querypath_core
- * @param mixed $document
- * A document in one of the forms listed above.
- * @param string $string
- * A CSS 3 selector.
- * @param array $options
- * An associative array of options. Currently supported options are listed above.
- * @return QueryPath
- */
- function qp($document = NULL, $string = NULL, $options = array()) {
-
- $qpClass = isset($options['QueryPath_class']) ? $options['QueryPath_class'] : 'QueryPath';
-
- $qp = new $qpClass($document, $string, $options);
- return $qp;
- }
- /**
- * A special-purpose version of {@link qp()} designed specifically for HTML.
- *
- * XHTML (if valid) can be easily parsed by {@link qp()} with no problems. However,
- * because of the way that libxml handles HTML, there are several common steps that
- * need to be taken to reliably parse non-XML HTML documents. This function is
- * a convenience tool for configuring QueryPath to parse HTML.
- *
- * The following options are automatically set unless overridden:
- * - ignore_parser_warnings: TRUE
- * - convert_to_encoding: ISO-8859-1 (the best for the HTML parser).
- * - convert_from_encoding: auto (autodetect encoding)
- * - use_parser: html
- *
- * Parser warning messages are also suppressed, so if the parser emits a warning,
- * the application will not be notified. This is equivalent to
- * calling @code@qp()@endcode.
- *
- * Warning: Character set conversions will only work if the Multi-Byte (mb) library
- * is installed and enabled. This is usually enabled, but not always.
- *
- * @ingroup querypath_core
- * @see qp()
- */
- function htmlqp($document = NULL, $selector = NULL, $options = array()) {
- // Need a way to force an HTML parse instead of an XML parse when the
- // doctype is XHTML, since many XHTML documents are not valid XML
- // (because of coding errors, not by design).
-
- $options += array(
- 'ignore_parser_warnings' => TRUE,
- 'convert_to_encoding' => 'ISO-8859-1',
- 'convert_from_encoding' => 'auto',
- //'replace_entities' => TRUE,
- 'use_parser' => 'html',
- // This is stripping actually necessary low ASCII.
- //'strip_low_ascii' => TRUE,
- );
- return @qp($document, $selector, $options);
- }
- /**
- * The Query Path object is the primary tool in this library.
- *
- * To create a new Query Path, use the {@link qp()} function.
- *
- * If you are new to these documents, start at the {@link QueryPath.php} page.
- * There you will find a quick guide to the tools contained in this project.
- *
- * A note on serialization: QueryPath uses DOM classes internally, and those
- * do not serialize well at all. In addition, QueryPath may contain many
- * extensions, and there is no guarantee that extensions can serialize. The
- * moral of the story: Don't serialize QueryPath.
- *
- * @see qp()
- * @see QueryPath.php
- * @ingroup querypath_core
- */
- class QueryPath implements IteratorAggregate, Countable {
-
- /**
- * The version string for this version of QueryPath.
- *
- * Standard releases will be of the following form: <MAJOR>.<MINOR>[.<PATCH>][-STABILITY].
- *
- * Examples:
- * - 2.0
- * - 2.1.1
- * - 2.0-alpha1
- *
- * Developer releases will always be of the form dev-<DATE>.
- *
- * @since 2.0
- */
- const VERSION = '-UNSTABLE%';
-
- /**
- * This is a stub HTML 4.01 document.
- *
- * <b>Using {@link QueryPath::XHTML_STUB} is preferred.</b>
- *
- * This is primarily for generating legacy HTML content. Modern web applications
- * should use {@link QueryPath::XHTML_STUB}.
- *
- * Use this stub with the HTML familiy of methods ({@link html()},
- * {@link writeHTML()}, {@link innerHTML()}).
- */
- const HTML_STUB = '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
- <html lang="en">
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
- <title>Untitled</title>
- </head>
- <body></body>
- </html>';
-
- /**
- * This is a stub XHTML document.
- *
- * Since XHTML is an XML format, you should use XML functions with this document
- * fragment. For example, you should use {@link xml()}, {@link innerXML()}, and
- * {@link writeXML()}.
- *
- * This can be passed into {@link qp()} to begin a new basic HTML document.
- *
- * Example:
- * @code
- * $qp = qp(QueryPath::XHTML_STUB); // Creates a new XHTML document
- * $qp->writeXML(); // Writes the document as well-formed XHTML.
- * @endcode
- * @since 2.0
- */
- const XHTML_STUB = '<?xml version="1.0"?>
- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
- <html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
- <title>Untitled</title>
- </head>
- <body></body>
- </html>';
-
- /**
- * Default parser flags.
- *
- * These are flags that will be used if no global or local flags override them.
- * @since 2.0
- */
- const DEFAULT_PARSER_FLAGS = NULL;
-
- const JS_CSS_ESCAPE_CDATA = '\\1';
- const JS_CSS_ESCAPE_CDATA_CCOMMENT = '/* \\1 */';
- const JS_CSS_ESCAPE_CDATA_DOUBLESLASH = '// \\1';
- const JS_CSS_ESCAPE_NONE = '';
-
- //const IGNORE_ERRORS = 1544; //E_NOTICE | E_USER_WARNING | E_USER_NOTICE;
- private $errTypes = 771; //E_ERROR; | E_USER_ERROR;
-
- /**
- * The base DOMDocument.
- */
- protected $document = NULL;
- private $options = array(
- 'parser_flags' => NULL,
- 'omit_xml_declaration' => FALSE,
- 'replace_entities' => FALSE,
- 'exception_level' => 771, // E_ERROR | E_USER_ERROR | E_USER_WARNING | E_WARNING
- 'ignore_parser_warnings' => FALSE,
- 'escape_xhtml_js_css_sections' => self::JS_CSS_ESCAPE_CDATA_CCOMMENT,
- );
- /**
- * The array of matches.
- */
- protected $matches = array();
- /**
- * The last array of matches.
- */
- protected $last = array(); // Last set of matches.
- private $ext = array(); // Extensions array.
-
- /**
- * The number of current matches.
- *
- * @see count()
- */
- public $length = 0;
-
- /**
- * Constructor.
- *
- * This should not be called directly. Use the {@link qp()} factory function
- * instead.
- *
- * @param mixed $document
- * A document-like object.
- * @param string $string
- * A CSS 3 Selector
- * @param array $options
- * An associative array of options.
- * @see qp()
- */
- public function __construct($document = NULL, $string = NULL, $options = array()) {
- $string = trim($string);
- $this->options = $options + QueryPathOptions::get() + $this->options;
-
- $parser_flags = isset($options['parser_flags']) ? $options['parser_flags'] : self::DEFAULT_PARSER_FLAGS;
- if (!empty($this->options['ignore_parser_warnings'])) {
- // Don't convert parser warnings into exceptions.
- $this->errTypes = 257; //E_ERROR | E_USER_ERROR;
- }
- elseif (isset($this->options['exception_level'])) {
- // Set the error level at which exceptions will be thrown. By default,
- // QueryPath will throw exceptions for
- // E_ERROR | E_USER_ERROR | E_WARNING | E_USER_WARNING.
- $this->errTypes = $this->options['exception_level'];
- }
-
- // Empty: Just create an empty QP.
- if (empty($document)) {
- $this->document = isset($this->options['encoding']) ? new DOMDocument('1.0', $this->options['encoding']) : new DOMDocument();
- $this->setMatches(new SplObjectStorage());
- }
- // Figure out if document is DOM, HTML/XML, or a filename
- elseif (is_object($document)) {
-
- if ($document instanceof QueryPath) {
- $this->matches = $document->get(NULL, TRUE);
- if ($this->matches->count() > 0)
- $this->document = $this->getFirstMatch()->ownerDocument;
- }
- elseif ($document instanceof DOMDocument) {
- $this->document = $document;
- //$this->matches = $this->matches($document->documentElement);
- $this->setMatches($document->documentElement);
- }
- elseif ($document instanceof DOMNode) {
- $this->document = $document->ownerDocument;
- //$this->matches = array($document);
- $this->setMatches($document);
- }
- elseif ($document instanceof SimpleXMLElement) {
- $import = dom_import_simplexml($document);
- $this->document = $import->ownerDocument;
- //$this->matches = array($import);
- $this->setMatches($import);
- }
- elseif ($document instanceof SplObjectStorage) {
- if ($document->count() == 0) {
- throw new QueryPathException('Cannot initialize QueryPath from an empty SplObjectStore');
- }
- $this->matches = $document;
- $this->document = $this->getFirstMatch()->ownerDocument;
- }
- else {
- throw new QueryPathException('Unsupported class type: ' . get_class($document));
- }
- }
- elseif (is_array($document)) {
- //trigger_error('Detected deprecated array support', E_USER_NOTICE);
- if (!empty($document) && $document[0] instanceof DOMNode) {
- $found = new SplObjectStorage();
- foreach ($document as $item) $found->attach($item);
- //$this->matches = $found;
- $this->setMatches($found);
- $this->document = $this->getFirstMatch()->ownerDocument;
- }
- }
- elseif ($this->isXMLish($document)) {
- // $document is a string with XML
- $this->document = $this->parseXMLString($document);
- $this->setMatches($this->document->documentElement);
- }
- else {
-
- // $document is a filename
- $context = empty($options['context']) ? NULL : $options['context'];
- $this->document = $this->parseXMLFile($document, $parser_flags, $context);
- $this->setMatches($this->document->documentElement);
- }
-
- // Do a find if the second param was set.
- if (isset($string) && strlen($string) > 0) {
- $this->find($string);
- }
- }
-
- /**
- * A static function for transforming data into a Data URL.
- *
- * This can be used to create Data URLs for injection into CSS, JavaScript, or other
- * non-XML/HTML content. If you are working with QP objects, you may want to use
- * {@link dataURL()} instead.
- *
- * @param mixed $data
- * The contents to inject as the data. The value can be any one of the following:
- * - A URL: If this is given, then the subsystem will read the content from that URL. THIS
- * MUST BE A FULL URL, not a relative path.
- * - A string of data: If this is given, then the subsystem will encode the string.
- * - A stream or file handle: If this is given, the stream's contents will be encoded
- * and inserted as data.
- * (Note that we make the assumption here that you would never want to set data to be
- * a URL. If this is an incorrect assumption, file a bug.)
- * @param string $mime
- * The MIME type of the document.
- * @param resource $context
- * A valid context. Use this only if you need to pass a stream context. This is only necessary
- * if $data is a URL. (See {@link stream_context_create()}).
- * @return
- * An encoded data URL.
- */
- public static function encodeDataURL($data, $mime = 'application/octet-stream', $context = NULL) {
- if (is_resource($data)) {
- $data = stream_get_contents($data);
- }
- elseif (filter_var($data, FILTER_VALIDATE_URL)) {
- $data = file_get_contents($data, FALSE, $context);
- }
-
- $encoded = base64_encode($data);
-
- return 'data:' . $mime . ';base64,' . $encoded;
- }
-
- /**
- * Get the effective options for the current QueryPath object.
- *
- * This returns an associative array of all of the options as set
- * for the current QueryPath object. This includes default options,
- * options directly passed in via {@link qp()} or the constructor,
- * an options set in the {@link QueryPathOptions} object.
- *
- * The order of merging options is this:
- * - Options passed in using {@link qp()} are highest priority, and will
- * override other options.
- * - Options set with {@link QueryPathOptions} will override default options,
- * but can be overridden by options passed into {@link qp()}.
- * - Default options will be used when no overrides are present.
- *
- * This function will return the options currently used, with the above option
- * overriding having been calculated already.
- *
- * @return array
- * An associative array of options, calculated from defaults and overridden
- * options.
- * @see qp()
- * @see QueryPathOptions::set()
- * @see QueryPathOptions::merge()
- * @since 2.0
- */
- public function getOptions() {
- return $this->options;
- }
-
- /**
- * Select the root element of the document.
- *
- * This sets the current match to the document's root element. For
- * practical purposes, this is the same as:
- * @code
- * qp($someDoc)->find(':root');
- * @endcode
- * However, since it doesn't invoke a parser, it has less overhead. It also
- * works in cases where the QueryPath has been reduced to zero elements (a
- * case that is not handled by find(':root') because there is no element
- * whose root can be found).
- *
- * @param string $selector
- * A selector. If this is supplied, QueryPath will navigate to the
- * document root and then run the query. (Added in QueryPath 2.0 Beta 2)
- * @return QueryPath
- * The QueryPath object, wrapping the root element (document element)
- * for the current document.
- */
- public function top($selector = NULL) {
- $this->setMatches($this->document->documentElement);
- // print '=====================' . PHP_EOL;
- // var_dump($this->document);
- // print '=====================' . PHP_EOL;
- return !empty($selector) ? $this->find($selector) : $this;
- }
-
- /**
- * Given a CSS Selector, find matching items.
- *
- * @param string $selector
- * CSS 3 Selector
- * @return QueryPath
- * @see filter()
- * @see is()
- * @todo If a find() returns zero matches, then a subsequent find() will
- * also return zero matches, even if that find has a selector like :root.
- * The reason for this is that the {@link QueryPathCssEventHandler} does
- * not set the root of the document tree if it cannot find any elements
- * from which to determine what the root is. The workaround is to use
- * {@link top()} to select the root element again.
- */
- public function find($selector) {
-
- // Optimize for ID/Class searches. These two take a long time
- // when a rdp is used. Using an XPath pushes work to C code.
- $ids = array();
-
- $regex = '/^#([\w-]+)$|^\.([\w-]+)$/'; // $1 is ID, $2 is class.
- //$regex = '/^#([\w-]+)$/';
- if (preg_match($regex, $selector, $ids) === 1) {
- // If $1 is a match, we have an ID.
- if (!empty($ids[1])) {
- $xpath = new DOMXPath($this->document);
- foreach ($this->matches as $item) {
-
- // For whatever reasons, the .// does not work correctly
- // if the selected element is the root element. So we have
- // an awful hack.
- if ($item->isSameNode($this->document->documentElement) ) {
- $xpathQuery = "//*[@id='{$ids[1]}']";
- }
- // This is the correct XPath query.
- else {
- $xpathQuery = ".//*[@id='{$ids[1]}']";
- }
- //$nl = $xpath->query("//*[@id='{$ids[1]}']", $item);
- //$nl = $xpath->query(".//*[@id='{$ids[1]}']", $item);
- $nl = $xpath->query($xpathQuery, $item);
- if ($nl->length > 0) {
- $this->setMatches($nl->item(0));
- break;
- }
- else {
- // If no match is found, we set an empty.
- $this->noMatches();
- }
- }
- }
- // Quick search for class values. While the XPath can't do it
- // all, it is faster than doing a recusive node search.
- else {
- $xpath = new DOMXPath($this->document);
- $found = new SplObjectStorage();
- foreach ($this->matches as $item) {
- // See comments on this in the #id code above.
- if ($item->isSameNode($this->document->documentElement) ) {
- $xpathQuery = "//*[@class]";
- }
- // This is the correct XPath query.
- else {
- $xpathQuery = ".//*[@class]";
- }
- $nl = $xpath->query($xpathQuery, $item);
- for ($i = 0; $i < $nl->length; ++$i) {
- $vals = explode(' ', $nl->item($i)->getAttribute('class'));
- if (in_array($ids[2], $vals)) $found->attach($nl->item($i));
- }
- }
- $this->setMatches($found);
- }
-
- return $this;
- }
-
-
- $query = new QueryPathCssEventHandler($this->matches);
- $query->find($selector);
- //$this->matches = $query->getMatches();
- $this->setMatches($query->getMatches());
- return $this;
- }
-
- /**
- * Execute an XPath query and store the results in the QueryPath.
- *
- * Most methods in this class support CSS 3 Selectors. Sometimes, though,
- * XPath provides a finer-grained query language. Use this to execute
- * XPath queries.
- *
- * Beware, though. QueryPath works best on DOM Elements, but an XPath
- * query can return other nodes, strings, and values. These may not work with
- * other QueryPath functions (though you will be able to access the
- * values with {@link get()}).
- *
- * @param string $query
- * An XPath query.
- * @param array $options
- * Currently supported options are:
- * - 'namespace_prefix': And XML namespace prefix to be used as the default. Used
- * in conjunction with 'namespace_uri'
- * - 'namespace_uri': The URI to be used as the default namespace URI. Used
- * with 'namespace_prefix'
- * @return QueryPath
- * A QueryPath object wrapping the results of the query.
- * @see find()
- * @author M Butcher
- * @author Xavier Prud'homme
- */
- public function xpath($query, $options = array()) {
- $xpath = new DOMXPath($this->document);
-
- // Register a default namespace.
- if (!empty($options['namespace_prefix']) && !empty($options['namespace_uri'])) {
- $xpath->registerNamespace($options['namespace_prefix'], $options['namespace_uri']);
- }
-
- $found = new SplObjectStorage();
- foreach ($this->matches as $item) {
- $nl = $xpath->query($query, $item);
- if ($nl->length > 0) {
- for ($i = 0; $i < $nl->length; ++$i) $found->attach($nl->item($i));
- }
- }
- $this->setMatches($found);
- return $this;
- }
-
- /**
- * Get the number of elements currently wrapped by this object.
- *
- * Note that there is no length property on this object.
- *
- * @return int
- * Number of items in the object.
- * @deprecated QueryPath now implements Countable, so use count().
- */
- public function size() {
- return $this->matches->count();
- }
-
- /**
- * Get the number of elements currently wrapped by this object.
- *
- * Since QueryPath is Countable, the PHP count() function can also
- * be used on a QueryPath.
- *
- * @code
- * <?php
- * count(qp($xml, 'div'));
- * ?>
- * @endcode
- *
- * @return int
- * The number of matches in the QueryPath.
- */
- public function count() {
- return $this->matches->count();
- }
-
- /**
- * Get one or all elements from this object.
- *
- * When called with no paramaters, this returns all objects wrapped by
- * the QueryPath. Typically, these are DOMElement objects (unless you have
- * used {@link map()}, {@link xpath()}, or other methods that can select
- * non-elements).
- *
- * When called with an index, it will return the item in the QueryPath with
- * that index number.
- *
- * Calling this method does not change the QueryPath (e.g. it is
- * non-destructive).
- *
- * You can use qp()->get() to iterate over all elements matched. You can
- * also iterate over qp() itself (QueryPath implementations must be Traversable).
- * In the later case, though, each item
- * will be wrapped in a QueryPath object. To learn more about iterating
- * in QueryPath, see {@link examples/techniques.php}.
- *
- * @param int $index
- * If specified, then only this index value will be returned. If this
- * index is out of bounds, a NULL will be returned.
- * @param boolean $asObject
- * If this is TRUE, an {@link SplObjectStorage} object will be returned
- * instead of an array. This is the preferred method for extensions to use.
- * @return mixed
- * If an index is passed, one element will be returned. If no index is
- * present, an array of all matches will be returned.
- * @see eq()
- * @see SplObjectStorage
- */
- public function get($index = NULL, $asObject = FALSE) {
- if (isset($index)) {
- return ($this->size() > $index) ? $this->getNthMatch($index) : NULL;
- }
- // Retain support for legacy.
- if (!$asObject) {
- $matches = array();
- foreach ($this->matches as $m) $matches[] = $m;
- return $matches;
- }
- return $this->matches;
- }
-
- /**
- * Get the DOMDocument that we currently work with.
- *
- * This returns the current DOMDocument. Any changes made to this document will be
- * accessible to QueryPath, as both will share access to the same object.
- *
- * @return DOMDocument
- */
- public function document() {
- return $this->document;
- }
-
- /**
- * On an XML document, load all XIncludes.
- *
- * @return QueryPath
- */
- public function xinclude() {
- $this->document->xinclude();
- return $this;
- }
-
- /**
- * Get all current elements wrapped in an array.
- * Compatibility function for jQuery 1.4, but identical to calling {@link get()}
- * with no parameters.
- *
- * @return array
- * An array of DOMNodes (typically DOMElements).
- */
- public function toArray() {
- return $this->get();
- }
- /**
- * Get/set an attribute.
- * - If no parameters are specified, this returns an associative array of all
- * name/value pairs.
- * - If both $name and $value are set, then this will set the attribute name/value
- * pair for all items in this object.
- * - If $name is set, and is an array, then
- * all attributes in the array will be set for all items in this object.
- * - If $name is a string and is set, then the attribute value will be returned.
- *
- * When an attribute value is retrieved, only the attribute value of the FIRST
- * match is returned.
- *
- * @param mixed $name
- * The name of the attribute or an associative array of name/value pairs.
- * @param string $value
- * A value (used only when setting an individual property).
- * @return mixed
- * If this was a setter request, return the QueryPath object. If this was
- * an access request (getter), return the string value.
- * @see removeAttr()
- * @see tag()
- * @see hasAttr()
- * @see hasClass()
- */
- public function attr($name = NULL, $value = NULL) {
-
- // Default case: Return all attributes as an assoc array.
- if (is_null($name)) {
- if ($this->matches->count() == 0) return NULL;
- $ele = $this->getFirstMatch();
- $buffer = array();
-
- // This does not appear to be part of the DOM
- // spec. Nor is it documented. But it works.
- foreach ($ele->attributes as $name => $attrNode) {
- $buffer[$name] = $attrNode->value;
- }
- return $buffer;
- }
-
- // multi-setter
- if (is_array($name)) {
- foreach ($name as $k => $v) {
- foreach ($this->matches as $m) $m->setAttribute($k, $v);
- }
- return $this;
- }
- // setter
- if (isset($value)) {
- foreach ($this->matches as $m) $m->setAttribute($name, $value);
- return $this;
- }
-
- //getter
- if ($this->matches->count() == 0) return NULL;
-
- // Special node type handler:
- if ($name == 'nodeType') {
- return $this->getFirstMatch()->nodeType;
- }
-
- // Always return first match's attr.
- return $this->getFirstMatch()->getAttribute($name);
- }
- /**
- * Check to see if the given attribute is present.
- *
- * This returns TRUE if <em>all</em> selected items have the attribute, or
- * FALSE if at least one item does not have the attribute.
- *
- * @param string $attrName
- * The attribute name.
- * @return boolean
- * TRUE if all matches have the attribute, FALSE otherwise.
- * @since 2.0
- * @see attr()
- * @see hasClass()
- */
- public function hasAttr($attrName) {
- foreach ($this->matches as $match) {
- if (!$match->hasAttribute($attrName)) return FALSE;
- }
- return TRUE;
- }
-
- /**
- * Set/get a CSS value for the current element(s).
- * This sets the CSS value for each element in the QueryPath object.
- * It does this by setting (or getting) the style attribute (without a namespace).
- *
- * For example, consider this code:
- * @code
- * <?php
- * qp(HTML_STUB, 'body')->css('background-color','red')->html();
- * ?>
- * @endcode
- * This will return the following HTML:
- * @code
- * <body style="background-color: red"/>
- * @endcode
- *
- * If no parameters are passed into this function, then the current style
- * element will be returned unparsed. Example:
- * @code
- * <?php
- * qp(HTML_STUB, 'body')->css('background-color','red')->css();
- * ?>
- * @endcode
- * This will return the following:
- * @code
- * background-color: red
- * @endcode
- *
- * As of QueryPath 2.1, existing style attributes will be merged with new attributes.
- * (In previous versions of QueryPath, a call to css() overwrite the existing style
- * values).
- *
- * @param mixed $name
- * If this is a string, it will be used as a CSS name. If it is an array,
- * this will assume it is an array of name/value pairs of CSS rules. It will
- * apply all rules to all elements in the set.
- * @param string $value
- * The value to set. This is only set if $name is a string.
- * @return QueryPath
- */
- public function css($name = NULL, $value = '') {
- if (empty($name)) {
- return $this->attr('style');
- }
-
- // Get any existing CSS.
- $css = array();
- foreach ($this->matches as $match) {
- $style = $match->getAttribute('style');
- if (!empty($style)) {
- // XXX: Is this sufficient?
- $style_array = explode(';', $style);
- foreach ($style_array as $item) {
- $item = trim($item);
-
- // Skip empty attributes.
- if (strlen($item) == 0) continue;
-
- list($css_att, $css_val) = explode(':',$item, 2);
- $css[$css_att] = trim($css_val);
- }
- }
- }
-
- if (is_array($name)) {
- // Use array_merge instead of + to preserve order.
- $css = array_merge($css, $name);
- }
- else {
- $css[$name] = $value;
- }
-
- // Collapse CSS into a string.
- $format = '%s: %s;';
- $css_string = '';
- foreach ($css as $n => $v) {
- $css_string .= sprintf($format, $n, trim($v));
- }
-
- $this->attr('style', $css_string);
- return $this;
- }
-
- /**
- * Insert or retrieve a Data URL.
- *
- * When called with just $attr, it will fetch the result, attempt to decode it, and
- * return an array with the MIME type and the application data.
- *
- * When called with both $attr and $data, it will inject the data into all selected elements
- * So @code$qp->dataURL('src', file_get_contents('my.png'), 'image/png')@endcode will inject
- * the given PNG image into the selected elements.
- *
- * The current implementation only knows how to encode and decode Base 64 data.
- *
- * Note that this is known *not* to work on IE 6, but should render fine in other browsers.
- *
- * @param string $attr
- * The name of the attribute.
- * @param mixed $data
- * The contents to inject as the data. The value can be any one of the following:
- * - A URL: If this is given, then the subsystem will read the content from that URL. THIS
- * MUST BE A FULL URL, not a relative path.
- * - A string of data: If this is given, then the subsystem will encode the string.
- * - A stream or file handle: If this is given, the stream's contents will be encoded
- * and inserted as data.
- * (Note that we make the assumption here that you would never want to set data to be
- * a URL. If this is an incorrect assumption, file a bug.)
- * @param string $mime
- * The MIME type of the document.
- * @param resource $context
- * A valid context. Use this only if you need to pass a stream context. This is only necessary
- * if $data is a URL. (See {@link stream_context_create()}).
- * @return
- * If this is called as a setter, this will return a QueryPath object. Otherwise, it
- * will attempt to fetch data out of the attribute and return that.
- * @see http://en.wikipedia.org/wiki/Data:_URL
- * @see attr()
- * @since 2.1
- */
- public function dataURL($attr, $data = NULL, $mime = 'application/octet-stream', $context = NULL) {
- if (is_null($data)) {
- // Attempt to fetch the data
- $data = $this->attr($attr);
- if (empty($data) || is_array($data) || strpos($data, 'data:') !== 0) {
- return;
- }
-
- // So 1 and 2 should be MIME types, and 3 should be the base64-encoded data.
- $regex = '/^data:([a-zA-Z0-9]+)\/([a-zA-Z0-9]+);base64,(.*)$/';
- $matches = array();
- preg_match($regex, $data, $matches);
-
- if (!empty($matches)) {
- $result = array(
- 'mime' => $matches[1] . '/' . $matches[2],
- 'data' => base64_decode($matches[3]),
- );
- return $result;
- }
- }
- else {
-
- $attVal = self::encodeDataURL($data, $mime, $context);
-
- return $this->attr($attr, $attVal);
-
- }
- }
-
-
- /**
- * Remove the named attribute from all elements in the current QueryPath.
- *
- * This will remove any attribute with the given name. It will do this on each
- * item currently wrapped by QueryPath.
- *
- * As is the case in jQuery, this operation is not considered destructive.
- *
- * @param string $name
- * Name of the parameter to remove.
- * @return QueryPath
- * The QueryPath object with the same elements.
- * @see attr()
- */
- public function removeAttr($name) {
- foreach ($this->matches as $m) {
- //if ($m->hasAttribute($name))
- $m->removeAttribute($name);
- }
- return $this;
- }
- /**
- * Reduce the matched set to just one.
- *
- * This will take a matched set and reduce it to just one item -- the item
- * at the index specified. This is a destructive operation, and can be undone
- * with {@link end()}.
- *
- * @param $index
- * The index of the element to keep. The rest will be
- * discarded.
- * @return QueryPath
- * @see get()
- * @see is()
- * @see end()
- */
- public function eq($index) {
- // XXX: Might there be a more efficient way of doing this?
- $this->setMatches($this->getNthMatch($index));
- return $this;
- }
- /**
- * Given a selector, this checks to see if the current set has one or more matches.
- *
- * Unlike jQuery's version, this supports full selectors (not just simple ones).
- *
- * @param string $selector
- * The selector to search for. As of QueryPath 2.1.1, this also supports passing a
- * DOMNode object.
- * @return boolean
- * TRUE if one or more elements match. FALSE if no match is found.
- * @see get()
- * @see eq()
- */
- public function is($selector) {
-
- if (is_object($selector)) {
- if ($selector instanceof DOMNode) {
- return count($this->matches) == 1 && $selector->isSameNode($this->get(0));
- }
- elseif ($selector instanceof Traversable) {
- if (count($selector) != count($this->matches)) {
- return FALSE;
- }
- // Without $seen, there is an edge case here if $selector contains the same object
- // more than once, but the counts are equal. For example, [a, a, a, a] will
- // pass an is() on [a, b, c, d]. We use the $seen SPLOS to prevent this.
- $seen = new SplObjectStorage();
- foreach ($selector as $item) {
- if (!$this->matches->contains($item) || $seen->contains($item)) {
- return FALSE;
- }
- $seen->attach($item);
- }
- return TRUE;
- }
- throw new Exception('Cannot compare an object to a QueryPath.');
- return FALSE;
- }
-
- foreach ($this->matches as $m) {
- $q = new QueryPathCssEventHandler($m);
- if ($q->find($selector)->getMatches()->count()) {
- return TRUE;
- }
- }
- return FALSE;
- }
- /**
- * Filter a list down to only elements that match the selector.
- * Use this, for example, to find all elements with a class, or with
- * certain children.
- *
- * @param string $selector
- * The selector to use as a filter.
- * @return QueryPath
- * The QueryPath with non-matching items filtered out.
- * @see filterLambda()
- * @see filterCallback()
- * @see map()
- * @see find()
- * @see is()
- */
- public function filter($selector) {
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) if (qp($m, NULL, $this->options)->is($selector)) $found->attach($m);
- $this->setMatches($found);
- return $this;
- }
- /**
- * Filter based on a lambda function.
- *
- * The function string will be executed as if it were the body of a
- * function. It is passed two arguments:
- * - $index: The index of the item.
- * - $item: The current Element.
- * If the function returns boolean FALSE, the item will be removed from
- * the list of elements. Otherwise it will be kept.
- *
- * Example:
- * @code
- * qp('li')->filterLambda('qp($item)->attr("id") == "test"');
- * @endcode
- *
- * The above would filter down the list to only an item whose ID is
- * 'text'.
- *
- * @param string $fn
- * Inline lambda function in a string.
- * @return QueryPath
- * @see filter()
- * @see map()
- * @see mapLambda()
- * @see filterCallback()
- */
- public function filterLambda($fn) {
- $function = create_function('$index, $item', $fn);
- $found = new SplObjectStorage();
- $i = 0;
- foreach ($this->matches as $item)
- if ($function($i++, $item) !== FALSE) $found->attach($item);
-
- $this->setMatches($found);
- return $this;
- }
-
- /**
- * Use regular expressions to filter based on the text content of matched elements.
- *
- * Only items that match the given regular expression will be kept. All others will
- * be removed.
- *
- * The regular expression is run against the <i>text content</i> (the PCDATA) of the
- * elements. This is a way of filtering elements based on their content.
- *
- * Example:
- * @code
- * <?xml version="1.0"?>
- * <div>Hello <i>World</i></div>
- * @endcode
- *
- * @code
- * <?php
- * // This will be 1.
- * qp($xml, 'div')->filterPreg('/World/')->size();
- * ?>
- * @endcode
- *
- * The return value above will be 1 because the text content of @codeqp($xml, 'div')@endcode is
- * @codeHello World@endcode.
- *
- * Compare this to the behavior of the <em>:contains()</em> CSS3 pseudo-class.
- *
- * @param string $regex
- * A regular expression.
- * @return QueryPath
- * @see filter()
- * @see filterCallback()
- * @see preg_match()
- */
- public function filterPreg($regex) {
-
- $found = new SplObjectStorage();
-
- foreach ($this->matches as $item) {
- if (preg_match($regex, $item->textContent) > 0) {
- $found->attach($item);
- }
- }
- $this->setMatches($found);
-
- return $this;
- }
- /**
- * Filter based on a callback function.
- *
- * A callback may be any of the following:
- * - a function: 'my_func'.
- * - an object/method combo: $obj, 'myMethod'
- * - a class/method combo: 'MyClass', 'myMethod'
- * Note that classes are passed in strings. Objects are not.
- *
- * Each callback is passed to arguments:
- * - $index: The index position of the object in the array.
- * - $item: The item to be operated upon.
- *
- * If the callback function returns FALSE, the item will be removed from the
- * set of matches. Otherwise the item will be considered a match and left alone.
- *
- * @param callback $callback.
- * A callback either as a string (function) or an array (object, method OR
- * classname, method).
- * @return QueryPath
- * Query path object augmented according to the function.
- * @see filter()
- * @see filterLambda()
- * @see map()
- * @see is()
- * @see find()
- */
- public function filterCallback($callback) {
- $found = new SplObjectStorage();
- $i = 0;
- if (is_callable($callback)) {
- foreach($this->matches as $item)
- if (call_user_func($callback, $i++, $item) !== FALSE) $found->attach($item);
- }
- else {
- throw new QueryPathException('The specified callback is not callable.');
- }
- $this->setMatches($found);
- return $this;
- }
- /**
- * Filter a list to contain only items that do NOT match.
- *
- * @param string $selector
- * A selector to use as a negation filter. If the filter is matched, the
- * element will be removed from the list.
- * @return QueryPath
- * The QueryPath object with matching items filtered out.
- * @see find()
- */
- public function not($selector) {
- $found = new SplObjectStorage();
- if ($selector instanceof DOMElement) {
- foreach ($this->matches as $m) if ($m !== $selector) $found->attach($m);
- }
- elseif (is_array($selector)) {
- foreach ($this->matches as $m) {
- if (!in_array($m, $selector, TRUE)) $found->attach($m);
- }
- }
- elseif ($selector instanceof SplObjectStorage) {
- foreach ($this->matches as $m) if ($selector->contains($m)) $found->attach($m);
- }
- else {
- foreach ($this->matches as $m) if (!qp($m, NULL, $this->options)->is($selector)) $found->attach($m);
- }
- $this->setMatches($found);
- return $this;
- }
- /**
- * Get an item's index.
- *
- * Given a DOMElement, get the index from the matches. This is the
- * converse of {@link get()}.
- *
- * @param DOMElement $subject
- * The item to match.
- *
- * @return mixed
- * The index as an integer (if found), or boolean FALSE. Since 0 is a
- * valid index, you should use strong equality (===) to test..
- * @see get()
- * @see is()
- */
- public function index($subject) {
-
- $i = 0;
- foreach ($this->matches as $m) {
- if ($m === $subject) {
- return $i;
- }
- ++$i;
- }
- return FALSE;
- }
- /**
- * Run a function on each item in a set.
- *
- * The mapping callback can return anything. Whatever it returns will be
- * stored as a match in the set, though. This means that afer a map call,
- * there is no guarantee that the elements in the set will behave correctly
- * with other QueryPath functions.
- *
- * Callback rules:
- * - If the callback returns NULL, the item will be removed from the array.
- * - If the callback returns an array, the entire array will be stored in
- * the results.
- * - If the callback returns anything else, it will be appended to the array
- * of matches.
- *
- * @param callback $callback
- * The function or callback to use. The callback will be passed two params:
- * - $index: The index position in the list of items wrapped by this object.
- * - $item: The current item.
- *
- * @return QueryPath
- * The QueryPath object wrapping a list of whatever values were returned
- * by each run of the callback.
- *
- * @see QueryPath::get()
- * @see filter()
- * @see find()
- */
- public function map($callback) {
- $found = new SplObjectStorage();
-
- if (is_callable($callback)) {
- $i = 0;
- foreach ($this->matches as $item) {
- $c = call_user_func($callback, $i, $item);
- if (isset($c)) {
- if (is_array($c) || $c instanceof Iterable) {
- foreach ($c as $retval) {
- if (!is_object($retval)) {
- $tmp = new stdClass();
- $tmp->textContent = $retval;
- $retval = $tmp;
- }
- $found->attach($retval);
- }
- }
- else {
- if (!is_object($c)) {
- $tmp = new stdClass();
- $tmp->textContent = $c;
- $c = $tmp;
- }
- $found->attach($c);
- }
- }
- ++$i;
- }
- }
- else {
- throw new QueryPathException('Callback is not callable.');
- }
- $this->setMatches($found, FALSE);
- return $this;
- }
- /**
- * Narrow the items in this object down to only a slice of the starting items.
- *
- * @param integer $start
- * Where in the list of matches to begin the slice.
- * @param integer $length
- * The number of items to include in the slice. If nothing is specified, the
- * all remaining matches (from $start onward) will be included in the sliced
- * list.
- * @return QueryPath
- * @see array_slice()
- */
- public function slice($start, $length = 0) {
- $end = $length;
- $found = new SplObjectStorage();
- if ($start >= $this->size()) {
- $this->setMatches($found);
- return $this;
- }
-
- $i = $j = 0;
- foreach ($this->matches as $m) {
- if ($i >= $start) {
- if ($end > 0 && $j >= $end) {
- break;
- }
- $found->attach($m);
- ++$j;
- }
- ++$i;
- }
-
- $this->setMatches($found);
- return $this;
- }
- /**
- * Run a callback on each item in the list of items.
- *
- * Rules of the callback:
- * - A callback is passed two variables: $index and $item. (There is no
- * special treatment of $this, as there is in jQuery.)
- * - You will want to pass $item by reference if it is not an
- * object (DOMNodes are all objects).
- * - A callback that returns FALSE will stop execution of the each() loop. This
- * works like break in a standard loop.
- * - A TRUE return value from the callback is analogous to a continue statement.
- * - All other return values are ignored.
- *
- * @param callback $callback
- * The callback to run.
- * @return QueryPath
- * The QueryPath.
- * @see eachLambda()
- * @see filter()
- * @see map()
- */
- public function each($callback) {
- if (is_callable($callback)) {
- $i = 0;
- foreach ($this->matches as $item) {
- if (call_user_func($callback, $i, $item) === FALSE) return $this;
- ++$i;
- }
- }
- else {
- throw new QueryPathException('Callback is not callable.');
- }
- return $this;
- }
- /**
- * An each() iterator that takes a lambda function.
- *
- * @param string $lambda
- * The lambda function. This will be passed ($index, &$item).
- * @return QueryPath
- * The QueryPath object.
- * @see each()
- * @see filterLambda()
- * @see filterCallback()
- * @see map()
- */
- public function eachLambda($lambda) {
- $index = 0;
- foreach ($this->matches as $item) {
- $fn = create_function('$index, &$item', $lambda);
- if ($fn($index, $item) === FALSE) return $this;
- ++$index;
- }
- return $this;
- }
- /**
- * Insert the given markup as the last child.
- *
- * The markup will be inserted into each match in the set.
- *
- * The same element cannot be inserted multiple times into a document. DOM
- * documents do not allow a single object to be inserted multiple times
- * into the DOM. To insert the same XML repeatedly, we must first clone
- * the object. This has one practical implication: Once you have inserted
- * an element into the object, you cannot further manipulate the original
- * element and expect the changes to be replciated in the appended object.
- * (They are not the same -- there is no shared reference.) Instead, you
- * will need to retrieve the appended object and operate on that.
- *
- * @param mixed $data
- * This can be either a string (the usual case), or a DOM Element.
- * @return QueryPath
- * The QueryPath object.
- * @see appendTo()
- * @see prepend()
- * @throws QueryPathException
- * Thrown if $data is an unsupported object type.
- */
- public function append($data) {
- $data = $this->prepareInsert($data);
- if (isset($data)) {
- if (empty($this->document->documentElement) && $this->matches->count() == 0) {
- // Then we assume we are writing to the doc root
- $this->document->appendChild($data);
- $found = new SplObjectStorage();
- $found->attach($this->document->documentElement);
- $this->setMatches($found);
- }
- else {
- // You can only append in item once. So in cases where we
- // need to append multiple times, we have to clone the node.
- foreach ($this->matches as $m) {
- // DOMDocumentFragments are even more troublesome, as they don't
- // always clone correctly. So we have to clone their children.
- if ($data instanceof DOMDocumentFragment) {
- foreach ($data->childNodes as $n)
- $m->appendChild($n->cloneNode(TRUE));
- }
- else {
- // Otherwise a standard clone will do.
- $m->appendChild($data->cloneNode(TRUE));
- }
-
- }
- }
-
- }
- return $this;
- }
- /**
- * Append the current elements to the destination passed into the function.
- *
- * This cycles through all of the current matches and appends them to
- * the context given in $destination. If a selector is provided then the
- * $destination is queried (using that selector) prior to the data being
- * appended. The data is then appended to the found items.
- *
- * @param QueryPath $dest
- * A QueryPath object that will be appended to.
- * @return QueryPath
- * The original QueryPath, unaltered. Only the destination QueryPath will
- * be modified.
- * @see append()
- * @see prependTo()
- * @throws QueryPathException
- * Thrown if $data is an unsupported object type.
- */
- public function appendTo(QueryPath $dest) {
- foreach ($this->matches as $m) $dest->append($m);
- return $this;
- }
- /**
- * Insert the given markup as the first child.
- *
- * The markup will be inserted into each match in the set.
- *
- * @param mixed $data
- * This can be either a string (the usual case), or a DOM Element.
- * @return QueryPath
- * @see append()
- * @see before()
- * @see after()
- * @see prependTo()
- * @throws QueryPathException
- * Thrown if $data is an unsupported object type.
- */
- public function prepend($data) {
- $data = $this->prepareInsert($data);
- if (isset($data)) {
- foreach ($this->matches as $m) {
- $ins = $data->cloneNode(TRUE);
- if ($m->hasChildNodes())
- $m->insertBefore($ins, $m->childNodes->item(0));
- else
- $m->appendChild($ins);
- }
- }
- return $this;
- }
- /**
- * Take all nodes in the current object and prepend them to the children nodes of
- * each matched node in the passed-in QueryPath object.
- *
- * This will iterate through each item in the current QueryPath object and
- * add each item to the beginning of the children of each element in the
- * passed-in QueryPath object.
- *
- * @see insertBefore()
- * @see insertAfter()
- * @see prepend()
- * @see appendTo()
- * @param QueryPath $dest
- * The destination QueryPath object.
- * @return QueryPath
- * The original QueryPath, unmodified. NOT the destination QueryPath.
- * @throws QueryPathException
- * Thrown if $data is an unsupported object type.
- */
- public function prependTo(QueryPath $dest) {
- foreach ($this->matches as $m) $dest->prepend($m);
- return $this;
- }
- /**
- * Insert the given data before each element in the current set of matches.
- *
- * This will take the give data (XML or HTML) and put it before each of the items that
- * the QueryPath object currently contains. Contrast this with after().
- *
- * @param mixed $data
- * The data to be inserted. This can be XML in a string, a DomFragment, a DOMElement,
- * or the other usual suspects. (See {@link qp()}).
- * @return QueryPath
- * Returns the QueryPath with the new modifications. The list of elements currently
- * selected will remain the same.
- * @see insertBefore()
- * @see after()
- * @see append()
- * @see prepend()
- * @throws QueryPathException
- * Thrown if $data is an unsupported object type.
- */
- public function before($data) {
- $data = $this->prepareInsert($data);
- foreach ($this->matches as $m) {
- $ins = $data->cloneNode(TRUE);
- $m->parentNode->insertBefore($ins, $m);
- }
-
- return $this;
- }
- /**
- * Insert the current elements into the destination document.
- * The items are inserted before each element in the given QueryPath document.
- * That is, they will be siblings with the current elements.
- *
- * @param QueryPath $dest
- * Destination QueryPath document.
- * @return QueryPath
- * The current QueryPath object, unaltered. Only the destination QueryPath
- * object is altered.
- * @see before()
- * @see insertAfter()
- * @see appendTo()
- * @throws QueryPathException
- * Thrown if $data is an unsupported object type.
- */
- public function insertBefore(QueryPath $dest) {
- foreach ($this->matches as $m) $dest->before($m);
- return $this;
- }
- /**
- * Insert the contents of the current QueryPath after the nodes in the
- * destination QueryPath object.
- *
- * @param QueryPath $dest
- * Destination object where the current elements will be deposited.
- * @return QueryPath
- * The present QueryPath, unaltered. Only the destination object is altered.
- * @see after()
- * @see insertBefore()
- * @see append()
- * @throws QueryPathException
- * Thrown if $data is an unsupported object type.
- */
- public function insertAfter(QueryPath $dest) {
- foreach ($this->matches as $m) $dest->after($m);
- return $this;
- }
- /**
- * Insert the given data after each element in the current QueryPath object.
- *
- * This inserts the element as a peer to the currently matched elements.
- * Contrast this with {@link append()}, which inserts the data as children
- * of matched elements.
- *
- * @param mixed $data
- * The data to be appended.
- * @return QueryPath
- * The QueryPath object (with the items inserted).
- * @see before()
- * @see append()
- * @throws QueryPathException
- * Thrown if $data is an unsupported object type.
- */
- public function after($data) {
- $data = $this->prepareInsert($data);
- foreach ($this->matches as $m) {
- $ins = $data->cloneNode(TRUE);
- if (isset($m->nextSibling))
- $m->parentNode->insertBefore($ins, $m->nextSibling);
- else
- $m->parentNode->appendChild($ins);
- }
- return $this;
- }
- /**
- * Replace the existing element(s) in the list with a new one.
- *
- * @param mixed $new
- * A DOMElement or XML in a string. This will replace all elements
- * currently wrapped in the QueryPath object.
- * @return QueryPath
- * The QueryPath object wrapping <b>the items that were removed</b>.
- * This remains consistent with the jQuery API.
- * @see append()
- * @see prepend()
- * @see before()
- * @see after()
- * @see remove()
- * @see replaceAll()
- */
- public function replaceWith($new) {
- $data = $this->prepareInsert($new);
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) {
- $parent = $m->parentNode;
- $parent->insertBefore($data->cloneNode(TRUE), $m);
- $found->attach($parent->removeChild($m));
- }
- $this->setMatches($found);
- return $this;
- }
- /**
- * Remove the parent element from the selected node or nodes.
- *
- * This takes the given list of nodes and "unwraps" them, moving them out of their parent
- * node, and then deleting the parent node.
- *
- * For example, consider this:
- *
- * @code
- * <root><wrapper><content/></wrapper></root>
- * @endcode
- *
- * Now we can run this code:
- * @code
- * qp($xml, 'content')->unwrap();
- * @endcode
- *
- * This will result in:
- *
- * @code
- * <root><content/></root>
- * @endcode
- * This is the opposite of {@link wrap()}.
- *
- * <b>The root element cannot be unwrapped.</b> It has no parents.
- * If you attempt to use unwrap on a root element, this will throw a QueryPathException.
- * (You can, however, "Unwrap" a child that is a direct descendant of the root element. This
- * will remove the root element, and replace the child as the root element. Be careful, though.
- * You cannot set more than one child as a root element.)
- *
- * @return QueryPath
- * The QueryPath object, with the same element(s) selected.
- * @throws QueryPathException
- * An exception is thrown if one attempts to unwrap a root element.
- * @see wrap()
- * @since 2.1
- * @author mbutcher
- */
- public function unwrap() {
-
- // We do this in two loops in order to
- // capture the case where two matches are
- // under the same parent. Othwerwise we might
- // remove a match before we can move it.
- $parents = new SplObjectStorage();
- foreach ($this->matches as $m) {
-
- // Cannot unwrap the root element.
- if ($m->isSameNode($m->ownerDocument->documentElement)) {
- throw new QueryPathException('Cannot unwrap the root element.');
- }
-
- // Move children to peer of parent.
- $parent = $m->parentNode;
- $old = $parent->removeChild($m);
- $parent->parentNode->insertBefore($old, $parent);
- $parents->attach($parent);
- }
-
- // Now that all the children are moved, we
- // remove all of the parents.
- foreach ($parents as $ele) {
- $ele->parentNode->removeChild($ele);
- }
-
- return $this;
- }
- /**
- * Wrap each element inside of the given markup.
- *
- * Markup is usually a string, but it can also be a DOMNode, a document
- * fragment, a SimpleXMLElement, or another QueryPath object (in which case
- * the first item in the list will be used.)
- *
- * @param mixed $markup
- * Markup that will wrap each element in the current list.
- * @return QueryPath
- * The QueryPath object with the wrapping changes made.
- * @see wrapAll()
- * @see wrapInner()
- */
- public function wrap($markup) {
- $data = $this->prepareInsert($markup);
-
- // If the markup passed in is empty, we don't do any wrapping.
- if (empty($data)) {
- return $this;
- }
-
- foreach ($this->matches as $m) {
- $copy = $data->firstChild->cloneNode(TRUE);
-
- // XXX: Should be able to avoid doing this over and over.
- if ($copy->hasChildNodes()) {
- $deepest = $this->deepestNode($copy);
- // FIXME: Does this need a different data structure?
- $bottom = $deepest[0];
- }
- else
- $bottom = $copy;
- $parent = $m->parentNode;
- $parent->insertBefore($copy, $m);
- $m = $parent->removeChild($m);
- $bottom->appendChild($m);
- //$parent->appendChild($copy);
- }
- return $this;
- }
- /**
- * Wrap all elements inside of the given markup.
- *
- * So all elements will be grouped together under this single marked up
- * item. This works by first determining the parent element of the first item
- * in the list. It then moves all of the matching elements under the wrapper
- * and inserts the wrapper where that first element was found. (This is in
- * accordance with the way jQuery works.)
- *
- * Markup is usually XML in a string, but it can also be a DOMNode, a document
- * fragment, a SimpleXMLElement, or another QueryPath object (in which case
- * the first item in the list will be used.)
- *
- * @param string $markup
- * Markup that will wrap all elements in the current list.
- * @return QueryPath
- * The QueryPath object with the wrapping changes made.
- * @see wrap()
- * @see wrapInner()
- */
- public function wrapAll($markup) {
- if ($this->matches->count() == 0) return;
-
- $data = $this->prepareInsert($markup);
-
- if (empty($data)) {
- return $this;
- }
-
- if ($data->hasChildNodes()) {
- $deepest = $this->deepestNode($data);
- // FIXME: Does this need fixing?
- $bottom = $deepest[0];
- }
- else
- $bottom = $data;
- $first = $this->getFirstMatch();
- $parent = $first->parentNode;
- $parent->insertBefore($data, $first);
- foreach ($this->matches as $m) {
- $bottom->appendChild($m->parentNode->removeChild($m));
- }
- return $this;
- }
- /**
- * Wrap the child elements of each item in the list with the given markup.
- *
- * Markup is usually a string, but it can also be a DOMNode, a document
- * fragment, a SimpleXMLElement, or another QueryPath object (in which case
- * the first item in the list will be used.)
- *
- * @param string $markup
- * Markup that will wrap children of each element in the current list.
- * @return QueryPath
- * The QueryPath object with the wrapping changes made.
- * @see wrap()
- * @see wrapAll()
- */
- public function wrapInner($markup) {
- $data = $this->prepareInsert($markup);
-
- // No data? Short circuit.
- if (empty($data)) return $this;
-
- if ($data->hasChildNodes()) {
- $deepest = $this->deepestNode($data);
- // FIXME: ???
- $bottom = $deepest[0];
- }
- else
- $bottom = $data;
-
- foreach ($this->matches as $m) {
- if ($m->hasChildNodes()) {
- while($m->firstChild) {
- $kid = $m->removeChild($m->firstChild);
- $bottom->appendChild($kid);
- }
- }
- $m->appendChild($data);
- }
- return $this;
- }
- /**
- * Reduce the set of matches to the deepest child node in the tree.
- *
- * This loops through the matches and looks for the deepest child node of all of
- * the matches. "Deepest", here, is relative to the nodes in the list. It is
- * calculated as the distance from the starting node to the most distant child
- * node. In other words, it is not necessarily the farthest node from the root
- * element, but the farthest note from the matched element.
- *
- * In the case where there are multiple nodes at the same depth, all of the
- * nodes at that depth will be included.
- *
- * @return QueryPath
- * The QueryPath wrapping the single deepest node.
- */
- public function deepest() {
- $deepest = 0;
- $winner = new SplObjectStorage();
- foreach ($this->matches as $m) {
- $local_deepest = 0;
- $local_ele = $this->deepestNode($m, 0, NULL, $local_deepest);
-
- // Replace with the new deepest.
- if ($local_deepest > $deepest) {
- $winner = new SplObjectStorage();
- foreach ($local_ele as $lele) $winner->attach($lele);
- $deepest = $local_deepest;
- }
- // Augument with other equally deep elements.
- elseif ($local_deepest == $deepest) {
- foreach ($local_ele as $lele)
- $winner->attach($lele);
- }
- }
- $this->setMatches($winner);
- return $this;
- }
-
- /**
- * A depth-checking function. Typically, it only needs to be
- * invoked with the first parameter. The rest are used for recursion.
- * @see deepest();
- * @param DOMNode $ele
- * The element.
- * @param int $depth
- * The depth guage
- * @param mixed $current
- * The current set.
- * @param DOMNode $deepest
- * A reference to the current deepest node.
- * @return array
- * Returns an array of DOM nodes.
- */
- protected function deepestNode(DOMNode $ele, $depth = 0, $current = NULL, &$deepest = NULL) {
- // FIXME: Should this use SplObjectStorage?
- if (!isset($current)) $current = array($ele);
- if (!isset($deepest)) $deepest = $depth;
- if ($ele->hasChildNodes()) {
- foreach ($ele->childNodes as $child) {
- if ($child->nodeType === XML_ELEMENT_NODE) {
- $current = $this->deepestNode($child, $depth + 1, $current, $deepest);
- }
- }
- }
- elseif ($depth > $deepest) {
- $current = array($ele);
- $deepest = $depth;
- }
- elseif ($depth === $deepest) {
- $current[] = $ele;
- }
- return $current;
- }
-
- /**
- * Prepare an item for insertion into a DOM.
- *
- * This handles a variety of boilerplate tasks that need doing before an
- * indeterminate object can be inserted into a DOM tree.
- * - If item is a string, this is converted into a document fragment and returned.
- * - If item is a QueryPath, then the first item is retrieved and this call function
- * is called recursivel.
- * - If the item is a DOMNode, it is imported into the current DOM if necessary.
- * - If the item is a SimpleXMLElement, it is converted into a DOM node and then
- * imported.
- *
- * @param mixed $item
- * Item to prepare for insert.
- * @return mixed
- * Returns the prepared item.
- * @throws QueryPathException
- * Thrown if the object passed in is not of a supprted object type.
- */
- protected function prepareInsert($item) {
- if(empty($item)) {
- return;
- }
- elseif (is_string($item)) {
- // If configured to do so, replace all entities.
- if ($this->options['replace_entities']) {
- $item = QueryPathEntities::replaceAllEntities($item);
- }
-
- $frag = $this->document->createDocumentFragment();
- try {
- set_error_handler(array('QueryPathParseException', 'initializeFromError'), $this->errTypes);
- $frag->appendXML($item);
- }
- // Simulate a finally block.
- catch (Exception $e) {
- restore_error_handler();
- throw $e;
- }
- restore_error_handler();
- return $frag;
- }
- elseif ($item instanceof QueryPath) {
- if ($item->size() == 0)
- return;
-
- return $this->prepareInsert($item->get(0));
- }
- elseif ($item instanceof DOMNode) {
- if ($item->ownerDocument !== $this->document) {
- // Deep clone this and attach it to this document
- $item = $this->document->importNode($item, TRUE);
- }
- return $item;
- }
- elseif ($item instanceof SimpleXMLElement) {
- $element = dom_import_simplexml($item);
- return $this->document->importNode($element, TRUE);
- }
- // What should we do here?
- //var_dump($item);
- throw new QueryPathException("Cannot prepare item of unsupported type: " . gettype($item));
- }
- /**
- * The tag name of the first element in the list.
- *
- * This returns the tag name of the first element in the list of matches. If
- * the list is empty, an empty string will be used.
- *
- * @see replaceAll()
- * @see replaceWith()
- * @return string
- * The tag name of the first element in the list.
- */
- public function tag() {
- return ($this->size() > 0) ? $this->getFirstMatch()->tagName : '';
- }
- /**
- * Remove any items from the list if they match the selector.
- *
- * In other words, each item that matches the selector will be remove
- * from the DOM document. The returned QueryPath wraps the list of
- * removed elements.
- *
- * If no selector is specified, this will remove all current matches from
- * the document.
- *
- * @param string $selector
- * A CSS Selector.
- * @return QueryPath
- * The Query path wrapping a list of removed items.
- * @see replaceAll()
- * @see replaceWith()
- * @see removeChildren()
- */
- public function remove($selector = NULL) {
- if(!empty($selector)) {
- // Do a non-destructive find.
- $query = new QueryPathCssEventHandler($this->matches);
- $query->find($selector);
- $matches = $query->getMatches();
- }
- else {
- $matches = $this->matches;
- }
- $found = new SplObjectStorage();
- foreach ($matches as $item) {
- // The item returned is (according to docs) different from
- // the one passed in, so we have to re-store it.
- $found->attach($item->parentNode->removeChild($item));
- }
-
- // Return a clone QueryPath with just the removed items. If
- // no items are found, this will return an empty QueryPath.
- return count($found) == 0 ? new QueryPath() : new QueryPath($found);
- }
- /**
- * This replaces everything that matches the selector with the first value
- * in the current list.
- *
- * This is the reverse of replaceWith.
- *
- * Unlike jQuery, QueryPath cannot assume a default document. Consequently,
- * you must specify the intended destination document. If it is omitted, the
- * present document is assumed to be tthe document. However, that can result
- * in undefined behavior if the selector and the replacement are not sufficiently
- * distinct.
- *
- * @param string $selector
- * The selector.
- * @param DOMDocument $document
- * The destination document.
- * @return QueryPath
- * The QueryPath wrapping the modified document.
- * @deprecated Due to the fact that this is not a particularly friendly method,
- * and that it can be easily replicated using {@see replaceWith()}, it is to be
- * considered deprecated.
- * @see remove()
- * @see replaceWith()
- */
- public function replaceAll($selector, DOMDocument $document) {
- $replacement = $this->size() > 0 ? $this->getFirstMatch() : $this->document->createTextNode('');
-
- $c = new QueryPathCssEventHandler($document);
- $c->find($selector);
- $temp = $c->getMatches();
- foreach ($temp as $item) {
- $node = $replacement->cloneNode();
- $node = $document->importNode($node);
- $item->parentNode->replaceChild($node, $item);
- }
- return qp($document, NULL, $this->options);
- }
- /**
- * Add more elements to the current set of matches.
- *
- * This begins the new query at the top of the DOM again. The results found
- * when running this selector are then merged into the existing results. In
- * this way, you can add additional elements to the existing set.
- *
- * @param string $selector
- * A valid selector.
- * @return QueryPath
- * The QueryPath object with the newly added elements.
- * @see append()
- * @see after()
- * @see andSelf()
- * @see end()
- */
- public function add($selector) {
-
- // This is destructive, so we need to set $last:
- $this->last = $this->matches;
-
- foreach (qp($this->document, $selector, $this->options)->get() as $item)
- $this->matches->attach($item);
- return $this;
- }
- /**
- * Revert to the previous set of matches.
- *
- * This will revert back to the last set of matches (before the last
- * "destructive" set of operations). This undoes any change made to the set of
- * matched objects. Functions like find() and filter() change the
- * list of matched objects. The end() function will revert back to the last set of
- * matched items.
- *
- * Note that functions that modify the document, but do not change the list of
- * matched objects, are not "destructive". Thus, calling append('something')->end()
- * will not undo the append() call.
- *
- * Only one level of changes is stored. Reverting beyond that will result in
- * an empty set of matches. Example:
- *
- * @code
- * // The line below returns the same thing as qp(document, 'p');
- * qp(document, 'p')->find('div')->end();
- * // This returns an empty array:
- * qp(document, 'p')->end();
- * // This returns an empty array:
- * qp(document, 'p')->find('div')->find('span')->end()->end();
- * @endcode
- *
- * The last one returns an empty array because only one level of changes is stored.
- *
- * @return QueryPath
- * A QueryPath object reflecting the list of matches prior to the last destructive
- * operation.
- * @see andSelf()
- * @see add()
- */
- public function end() {
- // Note that this does not use setMatches because it must set the previous
- // set of matches to empty array.
- $this->matches = $this->last;
- $this->last = new SplObjectStorage();
- return $this;
- }
- /**
- * Combine the current and previous set of matched objects.
- *
- * Example:
- *
- * @code
- * qp(document, 'p')->find('div')->andSelf();
- * @endcode
- *
- * The code above will contain a list of all p elements and all div elements that
- * are beneath p elements.
- *
- * @see end();
- * @return QueryPath
- * A QueryPath object with the results of the last two "destructive" operations.
- * @see add()
- * @see end()
- */
- public function andSelf() {
- // This is destructive, so we need to set $last:
- $last = $this->matches;
-
- foreach ($this->last as $item) $this->matches->attach($item);
-
- $this->last = $last;
- return $this;
- }
- /**
- * Remove all child nodes.
- *
- * This is equivalent to jQuery's empty() function. (However, empty() is a
- * PHP built-in, and cannot be used as a method name.)
- *
- * @return QueryPath
- * The QueryPath object with the child nodes removed.
- * @see replaceWith()
- * @see replaceAll()
- * @see remove()
- */
- public function removeChildren() {
- foreach ($this->matches as $m) {
- while($kid = $m->firstChild) {
- $m->removeChild($kid);
- }
- }
- return $this;
- }
- /**
- * Get the children of the elements in the QueryPath object.
- *
- * If a selector is provided, the list of children will be filtered through
- * the selector.
- *
- * @param string $selector
- * A valid selector.
- * @return QueryPath
- * A QueryPath wrapping all of the children.
- * @see removeChildren()
- * @see parent()
- * @see parents()
- * @see next()
- * @see prev()
- */
- public function children($selector = NULL) {
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) {
- foreach($m->childNodes as $c) {
- if ($c->nodeType == XML_ELEMENT_NODE) $found->attach($c);
- }
- }
- if (empty($selector)) {
- $this->setMatches($found);
- }
- else {
- $this->matches = $found; // Don't buffer this. It is temporary.
- $this->filter($selector);
- }
- return $this;
- }
- /**
- * Get all child nodes (not just elements) of all items in the matched set.
- *
- * It gets only the immediate children, not all nodes in the subtree.
- *
- * This does not process iframes. Xinclude processing is dependent on the
- * DOM implementation and configuration.
- *
- * @return QueryPath
- * A QueryPath object wrapping all child nodes for all elements in the
- * QueryPath object.
- * @see find()
- * @see text()
- * @see html()
- * @see innerHTML()
- * @see xml()
- * @see innerXML()
- */
- public function contents() {
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) {
- if (empty($m->childNodes)) continue; // Issue #51
- foreach ($m->childNodes as $c) {
- $found->attach($c);
- }
- }
- $this->setMatches($found);
- return $this;
- }
- /**
- * Get a list of siblings for elements currently wrapped by this object.
- *
- * This will compile a list of every sibling of every element in the
- * current list of elements.
- *
- * Note that if two siblings are present in the QueryPath object to begin with,
- * then both will be returned in the matched set, since they are siblings of each
- * other. In other words,if the matches contain a and b, and a and b are siblings of
- * each other, than running siblings will return a set that contains
- * both a and b.
- *
- * @param string $selector
- * If the optional selector is provided, siblings will be filtered through
- * this expression.
- * @return QueryPath
- * The QueryPath containing the matched siblings.
- * @see contents()
- * @see children()
- * @see parent()
- * @see parents()
- */
- public function siblings($selector = NULL) {
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) {
- $parent = $m->parentNode;
- foreach ($parent->childNodes as $n) {
- if ($n->nodeType == XML_ELEMENT_NODE && $n !== $m) {
- $found->attach($n);
- }
- }
- }
- if (empty($selector)) {
- $this->setMatches($found);
- }
- else {
- $this->matches = $found; // Don't buffer this. It is temporary.
- $this->filter($selector);
- }
- return $this;
- }
- /**
- * Find the closest element matching the selector.
- *
- * This finds the closest match in the ancestry chain. It first checks the
- * present element. If the present element does not match, this traverses up
- * the ancestry chain (e.g. checks each parent) looking for an item that matches.
- *
- * It is provided for jQuery 1.3 compatibility.
- * @param string $selector
- * A CSS Selector to match.
- * @return QueryPath
- * The set of matches.
- * @since 2.0
- */
- public function closest($selector) {
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) {
-
- if (qp($m, NULL, $this->options)->is($selector) > 0) {
- $found->attach($m);
- }
- else {
- while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
- $m = $m->parentNode;
- // Is there any case where parent node is not an element?
- if ($m->nodeType === XML_ELEMENT_NODE && qp($m, NULL, $this->options)->is($selector) > 0) {
- $found->attach($m);
- break;
- }
- }
- }
-
- }
- $this->setMatches($found);
- return $this;
- }
- /**
- * Get the immediate parent of each element in the QueryPath.
- *
- * If a selector is passed, this will return the nearest matching parent for
- * each element in the QueryPath.
- *
- * @param string $selector
- * A valid CSS3 selector.
- * @return QueryPath
- * A QueryPath object wrapping the matching parents.
- * @see children()
- * @see siblings()
- * @see parents()
- */
- public function parent($selector = NULL) {
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) {
- while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
- $m = $m->parentNode;
- // Is there any case where parent node is not an element?
- if ($m->nodeType === XML_ELEMENT_NODE) {
- if (!empty($selector)) {
- if (qp($m, NULL, $this->options)->is($selector) > 0) {
- $found->attach($m);
- break;
- }
- }
- else {
- $found->attach($m);
- break;
- }
- }
- }
- }
- $this->setMatches($found);
- return $this;
- }
- /**
- * Get all ancestors of each element in the QueryPath.
- *
- * If a selector is present, only matching ancestors will be retrieved.
- *
- * @see parent()
- * @param string $selector
- * A valid CSS 3 Selector.
- * @return QueryPath
- * A QueryPath object containing the matching ancestors.
- * @see siblings()
- * @see children()
- */
- public function parents($selector = NULL) {
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) {
- while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
- $m = $m->parentNode;
- // Is there any case where parent node is not an element?
- if ($m->nodeType === XML_ELEMENT_NODE) {
- if (!empty($selector)) {
- if (qp($m, NULL, $this->options)->is($selector) > 0)
- $found->attach($m);
- }
- else
- $found->attach($m);
- }
- }
- }
- $this->setMatches($found);
- return $this;
- }
- /**
- * Set or get the markup for an element.
- *
- * If $markup is set, then the giving markup will be injected into each
- * item in the set. All other children of that node will be deleted, and this
- * new code will be the only child or children. The markup MUST BE WELL FORMED.
- *
- * If no markup is given, this will return a string representing the child
- * markup of the first node.
- *
- * <b>Important:</b> This differs from jQuery's html() function. This function
- * returns <i>the current node</i> and all of its children. jQuery returns only
- * the children. This means you do not need to do things like this:
- * @code$qp->parent()->html()@endcode.
- *
- * By default, this is HTML 4.01, not XHTML. Use {@link xml()} for XHTML.
- *
- * @param string $markup
- * The text to insert.
- * @return mixed
- * A string if no markup was passed, or a QueryPath if markup was passed.
- * @see xml()
- * @see text()
- * @see contents()
- */
- public function html($markup = NULL) {
- if (isset($markup)) {
-
- if ($this->options['replace_entities']) {
- $markup = QueryPathEntities::replaceAllEntities($markup);
- }
-
- // Parse the HTML and insert it into the DOM
- //$doc = DOMDocument::loadHTML($markup);
- $doc = $this->document->createDocumentFragment();
- $doc->appendXML($markup);
- $this->removeChildren();
- $this->append($doc);
- return $this;
- }
- $length = $this->size();
- if ($length == 0) {
- return NULL;
- }
- // Only return the first item -- that's what JQ does.
- $first = $this->getFirstMatch();
- // Catch cases where first item is not a legit DOM object.
- if (!($first instanceof DOMNode)) {
- return NULL;
- }
-
- // Added by eabrand.
- if(!$first->ownerDocument->documentElement) {
- return NULL;
- }
-
- if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
- return $this->document->saveHTML();
- }
- // saveHTML cannot take a node and serialize it.
- return $this->document->saveXML($first);
- }
-
- /**
- * Fetch the HTML contents INSIDE of the first QueryPath item.
- *
- * <b>This behaves the way jQuery's @codehtml()@endcode function behaves.</b>
- *
- * This gets all children of the first match in QueryPath.
- *
- * Consider this fragment:
- * @code
- * <div>
- * test <p>foo</p> test
- * </div>
- * @endcode
- *
- * We can retrieve just the contents of this code by doing something like
- * this:
- * @code
- * qp($xml, 'div')->innerHTML();
- * @endcode
- *
- * This would return the following:
- * @codetest <p>foo</p> test@endcode
- *
- * @return string
- * Returns a string representation of the child nodes of the first
- * matched element.
- * @see html()
- * @see innerXML()
- * @see innerXHTML()
- * @since 2.0
- */
- public function innerHTML() {
- return $this->innerXML();
- }
-
- /**
- * Fetch child (inner) nodes of the first match.
- *
- * This will return the children of the present match. For an example,
- * see {@link innerHTML()}.
- *
- * @see innerHTML()
- * @see innerXML()
- * @return string
- * Returns a string of XHTML that represents the children of the present
- * node.
- * @since 2.0
- */
- public function innerXHTML() {
- $length = $this->size();
- if ($length == 0) {
- return NULL;
- }
- // Only return the first item -- that's what JQ does.
- $first = $this->getFirstMatch();
- // Catch cases where first item is not a legit DOM object.
- if (!($first instanceof DOMNode)) {
- return NULL;
- }
- elseif (!$first->hasChildNodes()) {
- return '';
- }
-
- $buffer = '';
- foreach ($first->childNodes as $child) {
- $buffer .= $this->document->saveXML($child, LIBXML_NOEMPTYTAG);
- }
-
- return $buffer;
- }
-
- /**
- * Fetch child (inner) nodes of the first match.
- *
- * This will return the children of the present match. For an example,
- * see {@link innerHTML()}.
- *
- * @see innerHTML()
- * @see innerXHTML()
- * @return string
- * Returns a string of XHTML that represents the children of the present
- * node.
- * @since 2.0
- */
- public function innerXML() {
- $length = $this->size();
- if ($length == 0) {
- return NULL;
- }
- // Only return the first item -- that's what JQ does.
- $first = $this->getFirstMatch();
- // Catch cases where first item is not a legit DOM object.
- if (!($first instanceof DOMNode)) {
- return NULL;
- }
- elseif (!$first->hasChildNodes()) {
- return '';
- }
-
- $buffer = '';
- foreach ($first->childNodes as $child) {
- $buffer .= $this->document->saveXML($child);
- }
-
- return $buffer;
- }
-
- /**
- * Retrieve the text of each match and concatenate them with the given separator.
- *
- * This has the effect of looping through all children, retrieving their text
- * content, and then concatenating the text with a separator.
- *
- * @param string $sep
- * The string used to separate text items. The default is a comma followed by a
- * space.
- * @param boolean $filterEmpties
- * If this is true, empty items will be ignored.
- * @return string
- * The text contents, concatenated together with the given separator between
- * every pair of items.
- * @see implode()
- * @see text()
- * @since 2.0
- */
- public function textImplode($sep = ', ', $filterEmpties = TRUE) {
- $tmp = array();
- foreach ($this->matches as $m) {
- $txt = $m->textContent;
- $trimmed = trim($txt);
- // If filter empties out, then we only add items that have content.
- if ($filterEmpties) {
- if (strlen($trimmed) > 0) $tmp[] = $txt;
- }
- // Else add all content, even if it's empty.
- else {
- $tmp[] = $txt;
- }
- }
- return implode($sep, $tmp);
- }
- /**
- * Get the text contents from just child elements.
- *
- * This is a specialized variant of textImplode() that implodes text for just the
- * child elements of the current element.
- *
- * @param string $separator
- * The separator that will be inserted between found text content.
- * @return string
- * The concatenated values of all children.
- */
- function childrenText($separator = ' ') {
- // Branch makes it non-destructive.
- return $this->branch()->xpath('descendant::text()')->textImplode($separator);
- }
- /**
- * Get or set the text contents of a node.
- * @param string $text
- * If this is not NULL, this value will be set as the text of the node. It
- * will replace any existing content.
- * @return mixed
- * A QueryPath if $text is set, or the text content if no text
- * is passed in as a pram.
- * @see html()
- * @see xml()
- * @see contents()
- */
- public function text($text = NULL) {
- if (isset($text)) {
- $this->removeChildren();
- $textNode = $this->document->createTextNode($text);
- foreach ($this->matches as $m) $m->appendChild($textNode);
- return $this;
- }
- // Returns all text as one string:
- $buf = '';
- foreach ($this->matches as $m) $buf .= $m->textContent;
- return $buf;
- }
- /**
- * Get or set the text before each selected item.
- *
- * If $text is passed in, the text is inserted before each currently selected item.
- *
- * If no text is given, this will return the concatenated text after each selected element.
- *
- * @code
- * <?php
- * $xml = '<?xml version="1.0"?><root>Foo<a>Bar</a><b/></root>';
- *
- * // This will return 'Foo'
- * qp($xml, 'a')->textBefore();
- *
- * // This will insert 'Baz' right before <b/>.
- * qp($xml, 'b')->textBefore('Baz');
- * ?>
- * @endcode
- *
- * @param string $text
- * If this is set, it will be inserted before each node in the current set of
- * selected items.
- * @return mixed
- * Returns the QueryPath object if $text was set, and returns a string (possibly empty)
- * if no param is passed.
- */
- public function textBefore($text = NULL) {
- if (isset($text)) {
- $textNode = $this->document->createTextNode($text);
- return $this->before($textNode);
- }
- $buffer = '';
- foreach ($this->matches as $m) {
- $p = $m;
- while (isset($p->previousSibling) && $p->previousSibling->nodeType == XML_TEXT_NODE) {
- $p = $p->previousSibling;
- $buffer .= $p->textContent;
- }
- }
- return $buffer;
- }
-
- public function textAfter($text = NULL) {
- if (isset($text)) {
- $textNode = $this->document->createTextNode($text);
- return $this->after($textNode);
- }
- $buffer = '';
- foreach ($this->matches as $m) {
- $n = $m;
- while (isset($n->nextSibling) && $n->nextSibling->nodeType == XML_TEXT_NODE) {
- $n = $n->nextSibling;
- $buffer .= $n->textContent;
- }
- }
- return $buffer;
- }
-
- /**
- * Set or get the value of an element's 'value' attribute.
- *
- * The 'value' attribute is common in HTML form elements. This is a
- * convenience function for accessing the values. Since this is not common
- * task on the server side, this method may be removed in future releases. (It
- * is currently provided for jQuery compatibility.)
- *
- * If a value is provided in the params, then the value will be set for all
- * matches. If no params are given, then the value of the first matched element
- * will be returned. This may be NULL.
- *
- * @deprecated Just use attr(). There's no reason to use this on the server.
- * @see attr()
- * @param string $value
- * @return mixed
- * Returns a QueryPath if a string was passed in, and a string if no string
- * was passed in. In the later case, an error will produce NULL.
- */
- public function val($value = NULL) {
- if (isset($value)) {
- $this->attr('value', $value);
- return $this;
- }
- return $this->attr('value');
- }
- /**
- * Set or get XHTML markup for an element or elements.
- *
- * This differs from {@link html()} in that it processes (and produces)
- * strictly XML 1.0 compliant markup.
- *
- * Like {@link xml()} and {@link html()}, this functions as both a
- * setter and a getter.
- *
- * This is a convenience function for fetching HTML in XML format.
- * It does no processing of the markup (such as schema validation).
- * @param string $markup
- * A string containing XML data.
- * @return mixed
- * If markup is passed in, a QueryPath is returned. If no markup is passed
- * in, XML representing the first matched element is returned.
- * @see html()
- * @see innerXHTML()
- */
- public function xhtml($markup = NULL) {
-
- // XXX: This is a minor reworking of the original xml() method.
- // This should be refactored, probably.
- // See http://github.com/technosophos/querypath/issues#issue/10
-
- $omit_xml_decl = $this->options['omit_xml_declaration'];
- if ($markup === TRUE) {
- // Basically, we handle the special case where we don't
- // want the XML declaration to be displayed.
- $omit_xml_decl = TRUE;
- }
- elseif (isset($markup)) {
- return $this->xml($markup);
- }
-
- $length = $this->size();
- if ($length == 0) {
- return NULL;
- }
-
- // Only return the first item -- that's what JQ does.
- $first = $this->getFirstMatch();
- // Catch cases where first item is not a legit DOM object.
- if (!($first instanceof DOMNode)) {
- return NULL;
- }
-
- if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
-
- // Has the unfortunate side-effect of stripping doctype.
- //$text = ($omit_xml_decl ? $this->document->saveXML($first->ownerDocument->documentElement, LIBXML_NOEMPTYTAG) : $this->document->saveXML(NULL, LIBXML_NOEMPTYTAG));
- $text = $this->document->saveXML(NULL, LIBXML_NOEMPTYTAG);
- }
- else {
- $text = $this->document->saveXML($first, LIBXML_NOEMPTYTAG);
- }
-
- // Issue #47: Using the old trick for removing the XML tag also removed the
- // doctype. So we remove it with a regex:
- if ($omit_xml_decl) {
- $text = preg_replace('/<\?xml\s[^>]*\?>/', '', $text);
- }
-
- // This is slightly lenient: It allows for cases where code incorrectly places content
- // inside of these supposedly unary elements.
- $unary = '/<(area|base|basefont|br|col|frame|hr|img|input|isindex|link|meta|param)(?(?=\s)([^>\/]+))><\/[^>]*>/i';
- $text = preg_replace($unary, '<\\1\\2 />', $text);
-
- // Experimental: Support for enclosing CDATA sections with comments to be both XML compat
- // and HTML 4/5 compat
- $cdata = '/(<!\[CDATA\[|\]\]>)/i';
- $replace = $this->options['escape_xhtml_js_css_sections'];
- $text = preg_replace($cdata, $replace, $text);
-
- return $text;
- }
- /**
- * Set or get the XML markup for an element or elements.
- *
- * Like {@link html()}, this functions in both a setter and a getter mode.
- *
- * In setter mode, the string passed in will be parsed and then appended to the
- * elements wrapped by this QueryPath object.When in setter mode, this parses
- * the XML using the DOMFragment parser. For that reason, an XML declaration
- * is not necessary.
- *
- * In getter mode, the first element wrapped by this QueryPath object will be
- * converted to an XML string and returned.
- *
- * @param string $markup
- * A string containing XML data.
- * @return mixed
- * If markup is passed in, a QueryPath is returned. If no markup is passed
- * in, XML representing the first matched element is returned.
- * @see xhtml()
- * @see html()
- * @see text()
- * @see content()
- * @see innerXML()
- */
- public function xml($markup = NULL) {
- $omit_xml_decl = $this->options['omit_xml_declaration'];
- if ($markup === TRUE) {
- // Basically, we handle the special case where we don't
- // want the XML declaration to be displayed.
- $omit_xml_decl = TRUE;
- }
- elseif (isset($markup)) {
- if ($this->options['replace_entities']) {
- $markup = QueryPathEntities::replaceAllEntities($markup);
- }
- $doc = $this->document->createDocumentFragment();
- $doc->appendXML($markup);
- $this->removeChildren();
- $this->append($doc);
- return $this;
- }
- $length = $this->size();
- if ($length == 0) {
- return NULL;
- }
- // Only return the first item -- that's what JQ does.
- $first = $this->getFirstMatch();
-
- // Catch cases where first item is not a legit DOM object.
- if (!($first instanceof DOMNode)) {
- return NULL;
- }
-
- if ($first instanceof DOMDocument || $first->isSameNode($first->ownerDocument->documentElement)) {
-
- return ($omit_xml_decl ? $this->document->saveXML($first->ownerDocument->documentElement) : $this->document->saveXML());
- }
- return $this->document->saveXML($first);
- }
- /**
- * Send the XML document to the client.
- *
- * Write the document to a file path, if given, or
- * to stdout (usually the client).
- *
- * This prints the entire document.
- *
- * @param string $path
- * The path to the file into which the XML should be written. if
- * this is NULL, data will be written to STDOUT, which is usually
- * sent to the remote browser.
- * @param int $options
- * (As of QueryPath 2.1) Pass libxml options to the saving mechanism.
- * @return QueryPath
- * The QueryPath object, unmodified.
- * @see xml()
- * @see innerXML()
- * @see writeXHTML()
- * @throws Exception
- * In the event that a file cannot be written, an Exception will be thrown.
- */
- public function writeXML($path = NULL, $options = NULL) {
- if ($path == NULL) {
- print $this->document->saveXML(NULL, $options);
- }
- else {
- try {
- set_error_handler(array('QueryPathIOException', 'initializeFromError'));
- $this->document->save($path, $options);
- }
- catch (Exception $e) {
- restore_error_handler();
- throw $e;
- }
- restore_error_handler();
- }
- return $this;
- }
- /**
- * Writes HTML to output.
- *
- * HTML is formatted as HTML 4.01, without strict XML unary tags. This is for
- * legacy HTML content. Modern XHTML should be written using {@link toXHTML()}.
- *
- * Write the document to stdout (usually the client) or to a file.
- *
- * @param string $path
- * The path to the file into which the XML should be written. if
- * this is NULL, data will be written to STDOUT, which is usually
- * sent to the remote browser.
- * @return QueryPath
- * The QueryPath object, unmodified.
- * @see html()
- * @see innerHTML()
- * @throws Exception
- * In the event that a file cannot be written, an Exception will be thrown.
- */
- public function writeHTML($path = NULL) {
- if ($path == NULL) {
- print $this->document->saveHTML();
- }
- else {
- try {
- set_error_handler(array('QueryPathParseException', 'initializeFromError'));
- $this->document->saveHTMLFile($path);
- }
- catch (Exception $e) {
- restore_error_handler();
- throw $e;
- }
- restore_error_handler();
- }
- return $this;
- }
-
- /**
- * Write an XHTML file to output.
- *
- * Typically, you should use this instead of {@link writeHTML()}.
- *
- * Currently, this functions identically to {@link toXML()} <i>except that</i>
- * it always uses closing tags (e.g. always @code<script></script>@endcode,
- * never @code<script/>@endcode). It will
- * write the file as well-formed XML. No XHTML schema validation is done.
- *
- * @see writeXML()
- * @see xml()
- * @see writeHTML()
- * @see innerXHTML()
- * @see xhtml()
- * @param string $path
- * The filename of the file to write to.
- * @return QueryPath
- * Returns the QueryPath, unmodified.
- * @throws Exception
- * In the event that the output file cannot be written, an exception is
- * thrown.
- * @since 2.0
- */
- public function writeXHTML($path = NULL) {
- return $this->writeXML($path, LIBXML_NOEMPTYTAG);
- /*
- if ($path == NULL) {
- print $this->document->saveXML(NULL, LIBXML_NOEMPTYTAG);
- }
- else {
- try {
- set_error_handler(array('QueryPathIOException', 'initializeFromError'));
- $this->document->save($path, LIBXML_NOEMPTYTAG);
- }
- catch (Exception $e) {
- restore_error_handler();
- throw $e;
- }
- restore_error_handler();
- }
- return $this;
- */
- }
- /**
- * Get the next sibling of each element in the QueryPath.
- *
- * If a selector is provided, the next matching sibling will be returned.
- *
- * @param string $selector
- * A CSS3 selector.
- * @return QueryPath
- * The QueryPath object.
- * @see nextAll()
- * @see prev()
- * @see children()
- * @see contents()
- * @see parent()
- * @see parents()
- */
- public function next($selector = NULL) {
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) {
- while (isset($m->nextSibling)) {
- $m = $m->nextSibling;
- if ($m->nodeType === XML_ELEMENT_NODE) {
- if (!empty($selector)) {
- if (qp($m, NULL, $this->options)->is($selector) > 0) {
- $found->attach($m);
- break;
- }
- }
- else {
- $found->attach($m);
- break;
- }
- }
- }
- }
- $this->setMatches($found);
- return $this;
- }
- /**
- * Get all siblings after an element.
- *
- * For each element in the QueryPath, get all siblings that appear after
- * it. If a selector is passed in, then only siblings that match the
- * selector will be included.
- *
- * @param string $selector
- * A valid CSS 3 selector.
- * @return QueryPath
- * The QueryPath object, now containing the matching siblings.
- * @see next()
- * @see prevAll()
- * @see children()
- * @see siblings()
- */
- public function nextAll($selector = NULL) {
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) {
- while (isset($m->nextSibling)) {
- $m = $m->nextSibling;
- if ($m->nodeType === XML_ELEMENT_NODE) {
- if (!empty($selector)) {
- if (qp($m, NULL, $this->options)->is($selector) > 0) {
- $found->attach($m);
- }
- }
- else {
- $found->attach($m);
- }
- }
- }
- }
- $this->setMatches($found);
- return $this;
- }
- /**
- * Get the next sibling before each element in the QueryPath.
- *
- * For each element in the QueryPath, this retrieves the previous sibling
- * (if any). If a selector is supplied, it retrieves the first matching
- * sibling (if any is found).
- *
- * @param string $selector
- * A valid CSS 3 selector.
- * @return QueryPath
- * A QueryPath object, now containing any previous siblings that have been
- * found.
- * @see prevAll()
- * @see next()
- * @see siblings()
- * @see children()
- */
- public function prev($selector = NULL) {
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) {
- while (isset($m->previousSibling)) {
- $m = $m->previousSibling;
- if ($m->nodeType === XML_ELEMENT_NODE) {
- if (!empty($selector)) {
- if (qp($m, NULL, $this->options)->is($selector)) {
- $found->attach($m);
- break;
- }
- }
- else {
- $found->attach($m);
- break;
- }
- }
- }
- }
- $this->setMatches($found);
- return $this;
- }
- /**
- * Get the previous siblings for each element in the QueryPath.
- *
- * For each element in the QueryPath, get all previous siblings. If a
- * selector is provided, only matching siblings will be retrieved.
- *
- * @param string $selector
- * A valid CSS 3 selector.
- * @return QueryPath
- * The QueryPath object, now wrapping previous sibling elements.
- * @see prev()
- * @see nextAll()
- * @see siblings()
- * @see contents()
- * @see children()
- */
- public function prevAll($selector = NULL) {
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) {
- while (isset($m->previousSibling)) {
- $m = $m->previousSibling;
- if ($m->nodeType === XML_ELEMENT_NODE) {
- if (!empty($selector)) {
- if (qp($m, NULL, $this->options)->is($selector)) {
- $found->attach($m);
- }
- }
- else {
- $found->attach($m);
- }
- }
- }
- }
- $this->setMatches($found);
- return $this;
- }
- /**
- * @deprecated Use {@link siblings()}.
- */
- public function peers($selector = NULL) {
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) {
- foreach ($m->parentNode->childNodes as $kid) {
- if ($kid->nodeType == XML_ELEMENT_NODE && $m !== $kid) {
- if (!empty($selector)) {
- if (qp($kid, NULL, $this->options)->is($selector)) {
- $found->attach($kid);
- }
- }
- else {
- $found->attach($kid);
- }
- }
- }
- }
- $this->setMatches($found);
- return $this;
- }
- /**
- * Add a class to all elements in the current QueryPath.
- *
- * This searchers for a class attribute on each item wrapped by the current
- * QueryPath object. If no attribute is found, a new one is added and its value
- * is set to $class. If a class attribute is found, then the value is appended
- * on to the end.
- *
- * @param string $class
- * The name of the class.
- * @return QueryPath
- * Returns the QueryPath object.
- * @see css()
- * @see attr()
- * @see removeClass()
- * @see hasClass()
- */
- public function addClass($class) {
- foreach ($this->matches as $m) {
- if ($m->hasAttribute('class')) {
- $val = $m->getAttribute('class');
- $m->setAttribute('class', $val . ' ' . $class);
- }
- else {
- $m->setAttribute('class', $class);
- }
- }
- return $this;
- }
- /**
- * Remove the named class from any element in the QueryPath that has it.
- *
- * This may result in the entire class attribute being removed. If there
- * are other items in the class attribute, though, they will not be removed.
- *
- * Example:
- * Consider this XML:
- * @code
- * <element class="first second"/>
- * @endcode
- *
- * Executing this fragment of code will remove only the 'first' class:
- * @code
- * qp(document, 'element')->removeClass('first');
- * @endcode
- *
- * The resulting XML will be:
- * @code
- * <element class="second"/>
- * @endcode
- *
- * To remove the entire 'class' attribute, you should use {@see removeAttr()}.
- *
- * @param string $class
- * The class name to remove.
- * @return QueryPath
- * The modified QueryPath object.
- * @see attr()
- * @see addClass()
- * @see hasClass()
- */
- public function removeClass($class) {
- foreach ($this->matches as $m) {
- if ($m->hasAttribute('class')) {
- $vals = explode(' ', $m->getAttribute('class'));
- if (in_array($class, $vals)) {
- $buf = array();
- foreach ($vals as $v) {
- if ($v != $class) $buf[] = $v;
- }
- if (count($buf) == 0)
- $m->removeAttribute('class');
- else
- $m->setAttribute('class', implode(' ', $buf));
- }
- }
- }
- return $this;
- }
- /**
- * Returns TRUE if any of the elements in the QueryPath have the specified class.
- *
- * @param string $class
- * The name of the class.
- * @return boolean
- * TRUE if the class exists in one or more of the elements, FALSE otherwise.
- * @see addClass()
- * @see removeClass()
- */
- public function hasClass($class) {
- foreach ($this->matches as $m) {
- if ($m->hasAttribute('class')) {
- $vals = explode(' ', $m->getAttribute('class'));
- if (in_array($class, $vals)) return TRUE;
- }
- }
- return FALSE;
- }
- /**
- * Branch the base QueryPath into another one with the same matches.
- *
- * This function makes a copy of the QueryPath object, but keeps the new copy
- * (initially) pointed at the same matches. This object can then be queried without
- * changing the original QueryPath. However, changes to the elements inside of this
- * QueryPath will show up in the QueryPath from which it is branched.
- *
- * Compare this operation with {@link cloneAll()}. The cloneAll() call takes
- * the current QueryPath object and makes a copy of all of its matches. You continue
- * to operate on the same QueryPath object, but the elements inside of the QueryPath
- * are copies of those before the call to cloneAll().
- *
- * This, on the other hand, copies <i>the QueryPath</i>, but keeps valid
- * references to the document and the wrapped elements. A new query branch is
- * created, but any changes will be written back to the same document.
- *
- * In practice, this comes in handy when you want to do multiple queries on a part
- * of the document, but then return to a previous set of matches. (see {@link QPTPL}
- * for examples of this in practice).
- *
- * Example:
- *
- * @code
- * <?php
- * $qp = qp(QueryPath::HTML_STUB);
- * $branch = $qp->branch();
- * $branch->find('title')->text('Title');
- * $qp->find('body')->text('This is the body')->writeHTML;
- * ?>
- * @endcode
- *
- * Notice that in the code, each of the QueryPath objects is doing its own
- * query. However, both are modifying the same document. The result of the above
- * would look something like this:
- *
- * @code
- * <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
- * <html xmlns="http://www.w3.org/1999/xhtml">
- * <head>
- * <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
- * <title>Title</title>
- * </head>
- * <body>This is the body</body>
- * </html>
- * @endcode
- *
- * Notice that while $qp and $banch were performing separate queries, they
- * both modified the same document.
- *
- * In jQuery or a browser-based solution, you generally do not need a branching
- * function because there is (implicitly) only one document. In QueryPath, there
- * is no implicit document. Every document must be explicitly specified (and,
- * in most cases, parsed -- which is costly). Branching makes it possible to
- * work on one document with multiple QueryPath objects.
- *
- * @param string $selector
- * If a selector is passed in, an additional {@link find()} will be executed
- * on the branch before it is returned. (Added in QueryPath 2.0.)
- * @return QueryPath
- * A copy of the QueryPath object that points to the same set of elements that
- * the original QueryPath was pointing to.
- * @since 1.1
- * @see cloneAll()
- * @see find()
- */
- public function branch($selector = NULL) {
- $temp = qp($this->matches, NULL, $this->options);
- if (isset($selector)) $temp->find($selector);
- return $temp;
- }
- /**
- * Perform a deep clone of each node in the QueryPath.
- *
- * This does not clone the QueryPath object, but instead clones the
- * list of nodes wrapped by the QueryPath. Every element is deeply
- * cloned.
- *
- * This method is analogous to jQuery's clone() method.
- *
- * This is a destructive operation, which means that end() will revert
- * the list back to the clone's original.
- * @see qp()
- * @return QueryPath
- */
- public function cloneAll() {
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) $found->attach($m->cloneNode(TRUE));
- $this->setMatches($found, FALSE);
- return $this;
- }
-
- /**
- * Clone the QueryPath.
- *
- * This makes a deep clone of the elements inside of the QueryPath.
- *
- * This clones only the QueryPathImpl, not all of the decorators. The
- * clone operator in PHP should handle the cloning of the decorators.
- */
- public function __clone() {
- //XXX: Should we clone the document?
-
- // Make sure we clone the kids.
- $this->cloneAll();
- }
- /**
- * Detach any items from the list if they match the selector.
- *
- * In other words, each item that matches the selector will be remove
- * from the DOM document. The returned QueryPath wraps the list of
- * removed elements.
- *
- * If no selector is specified, this will remove all current matches from
- * the document.
- *
- * @param string $selector
- * A CSS Selector.
- * @return QueryPath
- * The Query path wrapping a list of removed items.
- * @see replaceAll()
- * @see replaceWith()
- * @see removeChildren()
- * @since 2.1
- * @author eabrand
- */
- public function detach($selector = NULL) {
- if(!empty($selector))
- $this->find($selector);
- $found = new SplObjectStorage();
- $this->last = $this->matches;
- foreach ($this->matches as $item) {
- // The item returned is (according to docs) different from
- // the one passed in, so we have to re-store it.
- $found->attach($item->parentNode->removeChild($item));
- }
- $this->setMatches($found);
- return $this;
- }
- /**
- * Attach any items from the list if they match the selector.
- *
- * If no selector is specified, this will remove all current matches from
- * the document.
- *
- * @param QueryPath $dest
- * A QueryPath Selector.
- * @return QueryPath
- * The Query path wrapping a list of removed items.
- * @see replaceAll()
- * @see replaceWith()
- * @see removeChildren()
- * @since 2.1
- * @author eabrand
- */
- public function attach(QueryPath $dest) {
- foreach ($this->last as $m) $dest->append($m);
- return $this;
- }
-
- /**
- * Reduce the elements matched by QueryPath to only those which contain the given item.
- *
- * There are two ways in which this is different from jQuery's implementation:
- * - We allow ANY DOMNode, not just DOMElements. That means this will work on
- * processor instructions, text nodes, comments, etc.
- * - Unlike jQuery, this implementation of has() follows QueryPath standard behavior
- * and modifies the existing object. It does not create a brand new object.
- *
- * @param mixed $contained
- * - If $contained is a CSS selector (e.g. '#foo'), this will test to see
- * if the current QueryPath has any elements that contain items that match
- * the selector.
- * - If $contained is a DOMNode, then this will test to see if THE EXACT DOMNode
- * exists in the currently matched elements. (Note that you cannot match across DOM trees, even if it is the same document.)
- * @since 2.1
- * @author eabrand
- * @todo It would be trivially easy to add support for iterating over an array or Iterable of DOMNodes.
- */
- public function has($contained) {
- $found = new SplObjectStorage();
-
- // If it's a selector, we just get all of the DOMNodes that match the selector.
- $nodes = array();
- if (is_string($contained)) {
- // Get the list of nodes.
- $nodes = $this->branch($contained)->get();
- }
- elseif ($contained instanceof DOMNode) {
- // Make a list with one node.
- $nodes = array($contained);
- }
-
- // Now we go through each of the nodes that we are testing. We want to find
- // ALL PARENTS that are in our existing QueryPath matches. Those are the
- // ones we add to our new matches.
- foreach ($nodes as $original_node) {
- $node = $original_node;
- while (!empty($node)/* && $node != $node->ownerDocument*/) {
- if ($this->matches->contains($node)) {
- $found->attach($node);
- }
- $node = $node->parentNode;
- }
- }
-
- $this->setMatches($found);
- return $this;
- }
- /**
- * Empty everything within the specified element.
- *
- * A convenience function for removeChildren(). This is equivalent to jQuery's
- * empty() function. However, `empty` is a built-in in PHP, and cannot be used as a
- * function name.
- *
- * @return QueryPath
- * The QueryPath object with the newly emptied elements.
- * @see removeChildren()
- * @since 2.1
- * @author eabrand
- * @deprecated The removeChildren() function is the preferred method.
- */
- public function emptyElement() {
- $this->removeChildren();
- return $this;
- }
-
- /**
- * Get the even elements, so counter-intuitively 1, 3, 5, etc.
- *
- *
- *
- * @return QueryPath
- * A QueryPath wrapping all of the children.
- * @see removeChildren()
- * @see parent()
- * @see parents()
- * @see next()
- * @see prev()
- * @since 2.1
- * @author eabrand
- */
- public function even() {
- $found = new SplObjectStorage();
- $even = false;
- foreach ($this->matches as $m) {
- if ($even && $m->nodeType == XML_ELEMENT_NODE) $found->attach($m);
- $even = ($even) ? false : true;
- }
- $this->setMatches($found);
- $this->matches = $found; // Don't buffer this. It is temporary.
- return $this;
- }
- /**
- * Get the odd elements, so counter-intuitively 0, 2, 4, etc.
- *
- *
- *
- * @return QueryPath
- * A QueryPath wrapping all of the children.
- * @see removeChildren()
- * @see parent()
- * @see parents()
- * @see next()
- * @see prev()
- * @since 2.1
- * @author eabrand
- */
- public function odd() {
- $found = new SplObjectStorage();
- $odd = true;
- foreach ($this->matches as $m) {
- if ($odd && $m->nodeType == XML_ELEMENT_NODE) $found->attach($m);
- $odd = ($odd) ? false : true;
- }
- $this->setMatches($found);
- $this->matches = $found; // Don't buffer this. It is temporary.
- return $this;
- }
-
- /**
- * Get the first matching element.
- *
- *
- * @return QueryPath
- * A QueryPath wrapping all of the children.
- * @see next()
- * @see prev()
- * @since 2.1
- * @author eabrand
- */
- public function first() {
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) {
- if ($m->nodeType == XML_ELEMENT_NODE) {
- $found->attach($m);
- break;
- }
- }
- $this->setMatches($found);
- $this->matches = $found; // Don't buffer this. It is temporary.
- return $this;
- }
- /**
- * Get the first child of the matching element.
- *
- *
- * @return QueryPath
- * A QueryPath wrapping all of the children.
- * @see next()
- * @see prev()
- * @since 2.1
- * @author eabrand
- */
- public function firstChild() {
- // Could possibly use $m->firstChild http://theserverpages.com/php/manual/en/ref.dom.php
- $found = new SplObjectStorage();
- $flag = false;
- foreach ($this->matches as $m) {
- foreach($m->childNodes as $c) {
- if ($c->nodeType == XML_ELEMENT_NODE) {
- $found->attach($c);
- $flag = true;
- break;
- }
- }
- if($flag) break;
- }
- $this->setMatches($found);
- $this->matches = $found; // Don't buffer this. It is temporary.
- return $this;
- }
- /**
- * Get the last matching element.
- *
- *
- * @return QueryPath
- * A QueryPath wrapping all of the children.
- * @see next()
- * @see prev()
- * @since 2.1
- * @author eabrand
- */
- public function last() {
- $found = new SplObjectStorage();
- $item = null;
- foreach ($this->matches as $m) {
- if ($m->nodeType == XML_ELEMENT_NODE) {
- $item = $m;
- }
- }
- if ($item) {
- $found->attach($item);
- }
- $this->setMatches($found);
- $this->matches = $found; // Don't buffer this. It is temporary.
- return $this;
- }
- /**
- * Get the last child of the matching element.
- *
- *
- * @return QueryPath
- * A QueryPath wrapping all of the children.
- * @see next()
- * @see prev()
- * @since 2.1
- * @author eabrand
- */
- public function lastChild() {
- $found = new SplObjectStorage();
- $item = null;
- foreach ($this->matches as $m) {
- foreach($m->childNodes as $c) {
- if ($c->nodeType == XML_ELEMENT_NODE) {
- $item = $c;
- }
- }
- if ($item) {
- $found->attach($item);
- $item = null;
- }
- }
- $this->setMatches($found);
- $this->matches = $found; // Don't buffer this. It is temporary.
- return $this;
- }
- /**
- * Get all siblings after an element until the selector is reached.
- *
- * For each element in the QueryPath, get all siblings that appear after
- * it. If a selector is passed in, then only siblings that match the
- * selector will be included.
- *
- * @param string $selector
- * A valid CSS 3 selector.
- * @return QueryPath
- * The QueryPath object, now containing the matching siblings.
- * @see next()
- * @see prevAll()
- * @see children()
- * @see siblings()
- * @since 2.1
- * @author eabrand
- */
- public function nextUntil($selector = NULL) {
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) {
- while (isset($m->nextSibling)) {
- $m = $m->nextSibling;
- if ($m->nodeType === XML_ELEMENT_NODE) {
- if (!empty($selector)) {
- if (qp($m, NULL, $this->options)->is($selector) > 0) {
- break;
- }
- else {
- $found->attach($m);
- }
- }
- else {
- $found->attach($m);
- }
- }
- }
- }
- $this->setMatches($found);
- return $this;
- }
- /**
- * Get the previous siblings for each element in the QueryPath
- * until the selector is reached.
- *
- * For each element in the QueryPath, get all previous siblings. If a
- * selector is provided, only matching siblings will be retrieved.
- *
- * @param string $selector
- * A valid CSS 3 selector.
- * @return QueryPath
- * The QueryPath object, now wrapping previous sibling elements.
- * @see prev()
- * @see nextAll()
- * @see siblings()
- * @see contents()
- * @see children()
- * @since 2.1
- * @author eabrand
- */
- public function prevUntil($selector = NULL) {
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) {
- while (isset($m->previousSibling)) {
- $m = $m->previousSibling;
- if ($m->nodeType === XML_ELEMENT_NODE) {
- if (!empty($selector) && qp($m, NULL, $this->options)->is($selector))
- break;
- else
- $found->attach($m);
- }
- }
- }
- $this->setMatches($found);
- return $this;
- }
-
- /**
- * Get all ancestors of each element in the QueryPath until the selector is reached.
- *
- * If a selector is present, only matching ancestors will be retrieved.
- *
- * @see parent()
- * @param string $selector
- * A valid CSS 3 Selector.
- * @return QueryPath
- * A QueryPath object containing the matching ancestors.
- * @see siblings()
- * @see children()
- * @since 2.1
- * @author eabrand
- */
- public function parentsUntil($selector = NULL) {
- $found = new SplObjectStorage();
- foreach ($this->matches as $m) {
- while ($m->parentNode->nodeType !== XML_DOCUMENT_NODE) {
- $m = $m->parentNode;
- // Is there any case where parent node is not an element?
- if ($m->nodeType === XML_ELEMENT_NODE) {
- if (!empty($selector)) {
- if (qp($m, NULL, $this->options)->is($selector) > 0)
- break;
- else
- $found->attach($m);
- }
- else
- $found->attach($m);
- }
- }
- }
- $this->setMatches($found);
- return $this;
- }
-
- /////// INTERNAL FUNCTIONS ////////
-
-
- /**
- * Determine whether a given string looks like XML or not.
- *
- * Basically, this scans a portion of the supplied string, checking to see
- * if it has a tag-like structure. It is possible to "confuse" this, which
- * may subsequently result in parse errors, but in the vast majority of
- * cases, this method serves as a valid inicator of whether or not the
- * content looks like XML.
- *
- * Things that are intentional excluded:
- * - plain text with no markup.
- * - strings that look like filesystem paths.
- *
- * Subclasses SHOULD NOT OVERRIDE THIS. Altering it may be altering
- * core assumptions about how things work. Instead, classes should
- * override the constructor and pass in only one of the parsed types
- * that this class expects.
- */
- protected function isXMLish($string) {
- // Long strings will exhaust the regex engine, so we
- // grab a representative string.
- // $test = substr($string, 0, 255);
- return (strpos($string, '<') !== FALSE && strpos($string, '>') !== FALSE);
- //return preg_match(ML_EXP, $test) > 0;
- }
-
- private function parseXMLString($string, $flags = NULL) {
-
- $document = new DOMDocument('1.0');
- $lead = strtolower(substr($string, 0, 5)); // <?xml
- try {
- set_error_handler(array('QueryPathParseException', 'initializeFromError'), $this->errTypes);
-
- if (isset($this->options['convert_to_encoding'])) {
- // Is there another way to do this?
-
- $from_enc = isset($this->options['convert_from_encoding']) ? $this->options['convert_from_encoding'] : 'auto';
- $to_enc = $this->options['convert_to_encoding'];
-
- if (function_exists('mb_convert_encoding')) {
- $string = mb_convert_encoding($string, $to_enc, $from_enc);
- }
-
- }
-
- // This is to avoid cases where low ascii digits have slipped into HTML.
- // AFAIK, it should not adversly effect UTF-8 documents.
- if (!empty($this->options['strip_low_ascii'])) {
- $string = filter_var($string, FILTER_UNSAFE_RAW, FILTER_FLAG_ENCODE_LOW);
- }
-
- // Allow users to override parser settings.
- if (empty($this->options['use_parser'])) {
- $useParser = '';
- }
- else {
- $useParser = strtolower($this->options['use_parser']);
- }
-
- // If HTML parser is requested, we use it.
- if ($useParser == 'html') {
- $document->loadHTML($string);
- }
- // Parse as XML if it looks like XML, or if XML parser is requested.
- elseif ($lead == '<?xml' || $useParser == 'xml') {
- if ($this->options['replace_entities']) {
- $string = QueryPathEntities::replaceAllEntities($string);
- }
- $document->loadXML($string, $flags);
- }
- // In all other cases, we try the HTML parser.
- else {
- $document->loadHTML($string);
- }
- }
- // Emulate 'finally' behavior.
- catch (Exception $e) {
- restore_error_handler();
- throw $e;
- }
- restore_error_handler();
-
- if (empty($document)) {
- throw new QueryPathParseException('Unknown parser exception.');
- }
- return $document;
- }
-
- /**
- * EXPERT: Be very, very careful using this.
- * A utility function for setting the current set of matches.
- * It makes sure the last matches buffer is set (for end() and andSelf()).
- * @since 2.0
- */
- public function setMatches($matches, $unique = TRUE) {
- // This causes a lot of overhead....
- //if ($unique) $matches = self::unique($matches);
- $this->last = $this->matches;
-
- // Just set current matches.
- if ($matches instanceof SplObjectStorage) {
- $this->matches = $matches;
- }
- // This is likely legacy code that needs conversion.
- elseif (is_array($matches)) {
- trigger_error('Legacy array detected.');
- $tmp = new SplObjectStorage();
- foreach ($matches as $m) $tmp->attach($m);
- $this->matches = $tmp;
- }
- // For non-arrays, try to create a new match set and
- // add this object.
- else {
- $found = new SplObjectStorage();
- if (isset($matches)) $found->attach($matches);
- $this->matches = $found;
- }
-
- // EXPERIMENTAL: Support for qp()->length.
- $this->length = $this->matches->count();
- }
-
- /**
- * Set the match monitor to empty.
- *
- * This preserves history.
- *
- * @since 2.0
- */
- private function noMatches() {
- $this->setMatches(NULL);
- }
-
- /**
- * A utility function for retriving a match by index.
- *
- * The internal data structure used in QueryPath does not have
- * strong random access support, so we suppliment it with this method.
- */
- private function getNthMatch($index) {
- if ($index > $this->matches->count() || $index < 0) return;
-
- $i = 0;
- foreach ($this->matches as $m) {
- if ($i++ == $index) return $m;
- }
- }
-
- /**
- * Convenience function for getNthMatch(0).
- */
- private function getFirstMatch() {
- $this->matches->rewind();
- return $this->matches->current();
- }
-
- /**
- * Parse just a fragment of XML.
- * This will automatically prepend an <?xml ?> declaration before parsing.
- * @param string $string
- * Fragment to parse.
- * @return DOMDocumentFragment
- * The parsed document fragment.
- */
- /*
- private function parseXMLFragment($string) {
- $frag = $this->document->createDocumentFragment();
- $frag->appendXML($string);
- return $frag;
- }
- */
-
- /**
- * Parse an XML or HTML file.
- *
- * This attempts to autodetect the type of file, and then parse it.
- *
- * @param string $filename
- * The file name to parse.
- * @param int $flags
- * The OR-combined flags accepted by the DOM parser. See the PHP documentation
- * for DOM or for libxml.
- * @param resource $context
- * The stream context for the file IO. If this is set, then an alternate
- * parsing path is followed: The file is loaded by PHP's stream-aware IO
- * facilities, read entirely into memory, and then handed off to
- * {@link parseXMLString()}. On large files, this can have a performance impact.
- * @throws QueryPathParseException
- * Thrown when a file cannot be loaded or parsed.
- */
- private function parseXMLFile($filename, $flags = NULL, $context = NULL) {
-
- // If a context is specified, we basically have to do the reading in
- // two steps:
- if (!empty($context)) {
- try {
- set_error_handler(array('QueryPathParseException', 'initializeFromError'), $this->errTypes);
- $contents = file_get_contents($filename, FALSE, $context);
-
- }
- // Apparently there is no 'finally' in PHP, so we have to restore the error
- // handler this way:
- catch(Exception $e) {
- restore_error_handler();
- throw $e;
- }
- restore_error_handler();
-
- if ($contents == FALSE) {
- throw new QueryPathParseException(sprintf('Contents of the file %s could not be retrieved.', $filename));
- }
-
-
- /* This is basically unneccessary overhead, as it is not more
- * accurate than the existing method.
- if (isset($md['wrapper_type']) && $md['wrapper_type'] == 'http') {
- for ($i = 0; $i < count($md['wrapper_data']); ++$i) {
- if (stripos($md['wrapper_data'][$i], 'content-type:') !== FALSE) {
- $ct = trim(substr($md['wrapper_data'][$i], 12));
- if (stripos('text/html') === 0) {
- $this->parseXMLString($contents, $flags, 'text/html');
- }
- else {
- // We can't account for all of the mime types that have
- // an XML payload, so we set it to XML.
- $this->parseXMLString($contents, $flags, 'text/xml');
- }
- break;
- }
- }
- }
- */
-
- return $this->parseXMLString($contents, $flags);
- }
-
- $document = new DOMDocument();
- $lastDot = strrpos($filename, '.');
-
- $htmlExtensions = array(
- '.html' => 1,
- '.htm' => 1,
- );
-
- // Allow users to override parser settings.
- if (empty($this->options['use_parser'])) {
- $useParser = '';
- }
- else {
- $useParser = strtolower($this->options['use_parser']);
- }
-
- $ext = $lastDot !== FALSE ? strtolower(substr($filename, $lastDot)) : '';
-
- try {
- set_error_handler(array('QueryPathParseException', 'initializeFromError'), $this->errTypes);
-
- // If the parser is explicitly set to XML, use that parser.
- if ($useParser == 'xml') {
- $r = $document->load($filename, $flags);
- }
- // Otherwise, see if it looks like HTML.
- elseif (isset($htmlExtensions[$ext]) || $useParser == 'html') {
- // Try parsing it as HTML.
- $r = $document->loadHTMLFile($filename);
- }
- // Default to XML.
- else {
- $r = $document->load($filename, $flags);
- }
-
- }
- // Emulate 'finally' behavior.
- catch (Exception $e) {
- restore_error_handler();
- throw $e;
- }
- restore_error_handler();
-
-
-
- /*
- if ($r == FALSE) {
- $fmt = 'Failed to load file %s: %s (%s, %s)';
- $err = error_get_last();
- if ($err['type'] & self::IGNORE_ERRORS) {
- // Need to report these somehow...
- trigger_error($err['message'], E_USER_WARNING);
- }
- else {
- throw new QueryPathParseException(sprintf($fmt, $filename, $err['message'], $err['file'], $err['line']));
- }
-
- //throw new QueryPathParseException(sprintf($fmt, $filename, $err['message'], $err['file'], $err['line']));
- }
- */
- return $document;
- }
-
- /**
- * Call extension methods.
- *
- * This function is used to invoke extension methods. It searches the
- * registered extenstensions for a matching function name. If one is found,
- * it is executed with the arguments in the $arguments array.
- *
- * @throws QueryPathException
- * An exception is thrown if a non-existent method is called.
- */
- public function __call($name, $arguments) {
-
- if (!QueryPathExtensionRegistry::$useRegistry) {
- throw new QueryPathException("No method named $name found (Extensions disabled).");
- }
-
- // Loading of extensions is deferred until the first time a
- // non-core method is called. This makes constructing faster, but it
- // may make the first invocation of __call() slower (if there are
- // enough extensions.)
- //
- // The main reason for moving this out of the constructor is that most
- // new QueryPath instances do not use extensions. Charging qp() calls
- // with the additional hit is not a good idea.
- //
- // Also, this will at least limit the number of circular references.
- if (empty($this->ext)) {
- // Load the registry
- $this->ext = QueryPathExtensionRegistry::getExtensions($this);
- }
-
- // Note that an empty ext registry indicates that extensions are disabled.
- if (!empty($this->ext) && QueryPathExtensionRegistry::hasMethod($name)) {
- $owner = QueryPathExtensionRegistry::getMethodClass($name);
- $method = new ReflectionMethod($owner, $name);
- return $method->invokeArgs($this->ext[$owner], $arguments);
- }
- throw new QueryPathException("No method named $name found. Possibly missing an extension.");
- }
-
- /**
- * Dynamically generate certain properties.
- *
- * This is used primarily to increase jQuery compatibility by providing property-like
- * behaviors.
- *
- * Currently defined properties:
- * - length: Alias of {@link size()}.
- */
- /*
- public function __get($name) {
- switch ($name) {
- case 'length':
- return $this->size();
- default:
- throw new QueryPathException('Unknown or inaccessible property "' . $name . '" (via __get())');
- }
- }
- */
-
- /**
- * Get an iterator for the matches in this object.
- * @return Iterable
- * Returns an iterator.
- */
- public function getIterator() {
- $i = new QueryPathIterator($this->matches);
- $i->options = $this->options;
- return $i;
- }
- }
- /**
- * Perform various tasks on HTML/XML entities.
- *
- * @ingroup querypath_util
- */
- class QueryPathEntities {
-
- /**
- * This is three regexes wrapped into 1. The | divides them.
- * 1: Match any char-based entity. This will go in $matches[1]
- * 2: Match any num-based entity. This will go in $matches[2]
- * 3: Match any hex-based entry. This will go in $matches[3]
- * 4: Match any ampersand that is not an entity. This goes in $matches[4]
- * This last rule will only match if one of the previous two has not already
- * matched.
- * XXX: Are octal encodings for entities acceptable?
- */
- //protected static $regex = '/&([\w]+);|&#([\d]+);|&([\w]*[\s$]+)/m';
- protected static $regex = '/&([\w]+);|&#([\d]+);|&#(x[0-9a-fA-F]+);|(&)/m';
-
- /**
- * Replace all entities.
- * This will scan a string and will attempt to replace all
- * entities with their numeric equivalent. This will not work
- * with specialized entities.
- *
- * @param string $string
- * The string to perform replacements on.
- * @return string
- * Returns a string that is similar to the original one, but with
- * all entity replacements made.
- */
- public static function replaceAllEntities($string) {
- return preg_replace_callback(self::$regex, 'QueryPathEntities::doReplacement', $string);
- }
-
- /**
- * Callback for processing replacements.
- *
- * @param array $matches
- * The regular expression replacement array.
- */
- protected static function doReplacement($matches) {
- // See how the regex above works out.
- //print_r($matches);
- // From count, we can tell whether we got a
- // char, num, or bare ampersand.
- $count = count($matches);
- switch ($count) {
- case 2:
- // We have a character entity
- return '&#' . self::replaceEntity($matches[1]) . ';';
- case 3:
- case 4:
- // we have a numeric entity
- return '&#' . $matches[$count-1] . ';';
- case 5:
- // We have an unescaped ampersand.
- return '&';
- }
- }
-
- /**
- * Lookup an entity string's numeric equivalent.
- *
- * @param string $entity
- * The entity whose numeric value is needed.
- * @return int
- * The integer value corresponding to the entity.
- * @author Matt Butcher
- * @author Ryan Mahoney
- */
- public static function replaceEntity($entity) {
- return self::$entity_array[$entity];
- }
-
- /**
- * Conversion mapper for entities in HTML.
- * Large entity conversion table. This is
- * significantly broader in range than
- * get_html_translation_table(HTML_ENTITIES).
- *
- * This code comes from Rhizome ({@link http://code.google.com/p/sinciput})
- *
- * @see get_html_translation_table()
- */
- private static $entity_array = array(
- 'nbsp' => 160, 'iexcl' => 161, 'cent' => 162, 'pound' => 163,
- 'curren' => 164, 'yen' => 165, 'brvbar' => 166, 'sect' => 167,
- 'uml' => 168, 'copy' => 169, 'ordf' => 170, 'laquo' => 171,
- 'not' => 172, 'shy' => 173, 'reg' => 174, 'macr' => 175, 'deg' => 176,
- 'plusmn' => 177, 'sup2' => 178, 'sup3' => 179, 'acute' => 180,
- 'micro' => 181, 'para' => 182, 'middot' => 183, 'cedil' => 184,
- 'sup1' => 185, 'ordm' => 186, 'raquo' => 187, 'frac14' => 188,
- 'frac12' => 189, 'frac34' => 190, 'iquest' => 191, 'Agrave' => 192,
- 'Aacute' => 193, 'Acirc' => 194, 'Atilde' => 195, 'Auml' => 196,
- 'Aring' => 197, 'AElig' => 198, 'Ccedil' => 199, 'Egrave' => 200,
- 'Eacute' => 201, 'Ecirc' => 202, 'Euml' => 203, 'Igrave' => 204,
- 'Iacute' => 205, 'Icirc' => 206, 'Iuml' => 207, 'ETH' => 208,
- 'Ntilde' => 209, 'Ograve' => 210, 'Oacute' => 211, 'Ocirc' => 212,
- 'Otilde' => 213, 'Ouml' => 214, 'times' => 215, 'Oslash' => 216,
- 'Ugrave' => 217, 'Uacute' => 218, 'Ucirc' => 219, 'Uuml' => 220,
- 'Yacute' => 221, 'THORN' => 222, 'szlig' => 223, 'agrave' => 224,
- 'aacute' => 225, 'acirc' => 226, 'atilde' => 227, 'auml' => 228,
- 'aring' => 229, 'aelig' => 230, 'ccedil' => 231, 'egrave' => 232,
- 'eacute' => 233, 'ecirc' => 234, 'euml' => 235, 'igrave' => 236,
- 'iacute' => 237, 'icirc' => 238, 'iuml' => 239, 'eth' => 240,
- 'ntilde' => 241, 'ograve' => 242, 'oacute' => 243, 'ocirc' => 244,
- 'otilde' => 245, 'ouml' => 246, 'divide' => 247, 'oslash' => 248,
- 'ugrave' => 249, 'uacute' => 250, 'ucirc' => 251, 'uuml' => 252,
- 'yacute' => 253, 'thorn' => 254, 'yuml' => 255, 'quot' => 34,
- 'amp' => 38, 'lt' => 60, 'gt' => 62, 'apos' => 39, 'OElig' => 338,
- 'oelig' => 339, 'Scaron' => 352, 'scaron' => 353, 'Yuml' => 376,
- 'circ' => 710, 'tilde' => 732, 'ensp' => 8194, 'emsp' => 8195,
- 'thinsp' => 8201, 'zwnj' => 8204, 'zwj' => 8205, 'lrm' => 8206,
- 'rlm' => 8207, 'ndash' => 8211, 'mdash' => 8212, 'lsquo' => 8216,
- 'rsquo' => 8217, 'sbquo' => 8218, 'ldquo' => 8220, 'rdquo' => 8221,
- 'bdquo' => 8222, 'dagger' => 8224, 'Dagger' => 8225, 'permil' => 8240,
- 'lsaquo' => 8249, 'rsaquo' => 8250, 'euro' => 8364, 'fnof' => 402,
- 'Alpha' => 913, 'Beta' => 914, 'Gamma' => 915, 'Delta' => 916,
- 'Epsilon' => 917, 'Zeta' => 918, 'Eta' => 919, 'Theta' => 920,
- 'Iota' => 921, 'Kappa' => 922, 'Lambda' => 923, 'Mu' => 924, 'Nu' => 925,
- 'Xi' => 926, 'Omicron' => 927, 'Pi' => 928, 'Rho' => 929, 'Sigma' => 931,
- 'Tau' => 932, 'Upsilon' => 933, 'Phi' => 934, 'Chi' => 935, 'Psi' => 936,
- 'Omega' => 937, 'alpha' => 945, 'beta' => 946, 'gamma' => 947,
- 'delta' => 948, 'epsilon' => 949, 'zeta' => 950, 'eta' => 951,
- 'theta' => 952, 'iota' => 953, 'kappa' => 954, 'lambda' => 955,
- 'mu' => 956, 'nu' => 957, 'xi' => 958, 'omicron' => 959, 'pi' => 960,
- 'rho' => 961, 'sigmaf' => 962, 'sigma' => 963, 'tau' => 964,
- 'upsilon' => 965, 'phi' => 966, 'chi' => 967, 'psi' => 968,
- 'omega' => 969, 'thetasym' => 977, 'upsih' => 978, 'piv' => 982,
- 'bull' => 8226, 'hellip' => 8230, 'prime' => 8242, 'Prime' => 8243,
- 'oline' => 8254, 'frasl' => 8260, 'weierp' => 8472, 'image' => 8465,
- 'real' => 8476, 'trade' => 8482, 'alefsym' => 8501, 'larr' => 8592,
- 'uarr' => 8593, 'rarr' => 8594, 'darr' => 8595, 'harr' => 8596,
- 'crarr' => 8629, 'lArr' => 8656, 'uArr' => 8657, 'rArr' => 8658,
- 'dArr' => 8659, 'hArr' => 8660, 'forall' => 8704, 'part' => 8706,
- 'exist' => 8707, 'empty' => 8709, 'nabla' => 8711, 'isin' => 8712,
- 'notin' => 8713, 'ni' => 8715, 'prod' => 8719, 'sum' => 8721,
- 'minus' => 8722, 'lowast' => 8727, 'radic' => 8730, 'prop' => 8733,
- 'infin' => 8734, 'ang' => 8736, 'and' => 8743, 'or' => 8744, 'cap' => 8745,
- 'cup' => 8746, 'int' => 8747, 'there4' => 8756, 'sim' => 8764,
- 'cong' => 8773, 'asymp' => 8776, 'ne' => 8800, 'equiv' => 8801,
- 'le' => 8804, 'ge' => 8805, 'sub' => 8834, 'sup' => 8835, 'nsub' => 8836,
- 'sube' => 8838, 'supe' => 8839, 'oplus' => 8853, 'otimes' => 8855,
- 'perp' => 8869, 'sdot' => 8901, 'lceil' => 8968, 'rceil' => 8969,
- 'lfloor' => 8970, 'rfloor' => 8971, 'lang' => 9001, 'rang' => 9002,
- 'loz' => 9674, 'spades' => 9824, 'clubs' => 9827, 'hearts' => 9829,
- 'diams' => 9830
- );
- }
- /**
- * An iterator for QueryPath.
- *
- * This provides iterator support for QueryPath. You do not need to construct
- * a QueryPathIterator. QueryPath does this when its {@link QueryPath::getIterator()}
- * method is called.
- *
- * @ingroup querypath_util
- */
- class QueryPathIterator extends IteratorIterator {
- public $options = array();
- private $qp = NULL;
-
- public function current() {
- if (!isset($this->qp)) {
- $this->qp = qp(parent::current(), NULL, $this->options);
- }
- else {
- $splos = new SplObjectStorage();
- $splos->attach(parent::current());
- $this->qp->setMatches($splos);
- }
- return $this->qp;
- }
- }
- /**
- * Manage default options.
- *
- * This class stores the default options for QueryPath. When a new
- * QueryPath object is constructed, options specified here will be
- * used.
- *
- * <b>Details</b>
- * This class defines no options of its own. Instead, it provides a
- * central tool for developers to override options set by QueryPath.
- * When a QueryPath object is created, it will evaluate options in the
- * following order:
- *
- * - Options passed into {@link qp()} have highest priority.
- * - Options in {@link QueryPathOptions} (this class) have the next highest priority.
- * - If the option is not specified elsewhere, QueryPath will use its own defaults.
- *
- * @see qp()
- * @see QueryPathOptions::set()
- * @ingroup querypath_util
- */
- class QueryPathOptions {
-
- /**
- * This is the static options array.
- *
- * Use the {@link set()}, {@link get()}, and {@link merge()} to
- * modify this array.
- */
- static $options = array();
-
- /**
- * Set the default options.
- *
- * The passed-in array will be used as the default options list.
- *
- * @param array $array
- * An associative array of options.
- */
- static function set($array) {
- self::$options = $array;
- }
-
- /**
- * Get the default options.
- *
- * Get all options currently set as default.
- *
- * @return array
- * An array of options. Note that only explicitly set options are
- * returned. {@link QueryPath} defines default options which are not
- * stored in this object.
- */
- static function get() {
- return self::$options;
- }
-
- /**
- * Merge the provided array with existing options.
- *
- * On duplicate keys, the value in $array will overwrite the
- * value stored in the options.
- *
- * @param array $array
- * Associative array of options to merge into the existing options.
- */
- static function merge($array) {
- self::$options = $array + self::$options;
- }
-
- /**
- * Returns true of the specified key is already overridden in this object.
- *
- * @param string $key
- * The key to search for.
- */
- static function has($key) {
- return array_key_exists($key, self::$options);
- }
-
- }
- /**
- * Exception indicating that a problem has occured inside of a QueryPath object.
- *
- * @ingroup querypath_core
- */
- class QueryPathException extends Exception {}
- /**
- * Exception indicating that a parser has failed to parse a file.
- *
- * This will report parser warnings as well as parser errors. It should only be
- * thrown, though, under error conditions.
- *
- * @ingroup querypath_core
- */
- class QueryPathParseException extends QueryPathException {
- const ERR_MSG_FORMAT = 'Parse error in %s on line %d column %d: %s (%d)';
- const WARN_MSG_FORMAT = 'Parser warning in %s on line %d column %d: %s (%d)';
- // trigger_error
- public function __construct($msg = '', $code = 0, $file = NULL, $line = NULL) {
- $msgs = array();
- foreach(libxml_get_errors() as $err) {
- $format = $err->level == LIBXML_ERR_WARNING ? self::WARN_MSG_FORMAT : self::ERR_MSG_FORMAT;
- $msgs[] = sprintf($format, $err->file, $err->line, $err->column, $err->message, $err->code);
- }
- $msg .= implode("\n", $msgs);
-
- if (isset($file)) {
- $msg .= ' (' . $file;
- if (isset($line)) $msg .= ': ' . $line;
- $msg .= ')';
- }
-
- parent::__construct($msg, $code);
- }
-
- public static function initializeFromError($code, $str, $file, $line, $cxt) {
- //printf("\n\nCODE: %s %s\n\n", $code, $str);
- $class = __CLASS__;
- throw new $class($str, $code, $file, $line);
- }
- }
- /**
- * Indicates that an input/output exception has occurred.
- *
- * @ingroup querypath_core
- */
- class QueryPathIOException extends QueryPathParseException {
- public static function initializeFromError($code, $str, $file, $line, $cxt) {
- $class = __CLASS__;
- throw new $class($str, $code, $file, $line);
- }
-
- }
|