diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml new file mode 100644 index 00000000..6a450c24 --- /dev/null +++ b/.github/workflows/examples.yml @@ -0,0 +1,39 @@ +name: Examples + +on: + schedule: + # Weekly, on Monday morning. + - cron: "0 7 * * 1" + workflow_dispatch: + pull_request: + paths: + - 'examples/**' + - 'tests/QueryPath/ExampleRunner.php' + - 'tests/run-examples.php' + - '.github/workflows/examples.yml' + +jobs: + network: + name: Network examples + runs-on: ubuntu-latest + + # These call third-party services, so they can fail for reasons that have + # nothing to do with a change here. The examples retry when an API asks them + # to slow down; anything that still fails is treated as a real problem. + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + ini-values: error_reporting=E_ALL + extensions: dom, simplexml, zip, curl + + - name: Install Composer Dependencies + uses: ramsey/composer-install@v4 + + - name: Run the examples that call remote services + run: composer run test:examples:network diff --git a/.github/workflows/unit-testing.yml b/.github/workflows/unit-testing.yml index b92fc095..be8b87d8 100644 --- a/.github/workflows/unit-testing.yml +++ b/.github/workflows/unit-testing.yml @@ -22,6 +22,8 @@ jobs: with: php-version: ${{ matrix.php-versions }} ini-values: error_reporting=E_ALL + # zip is needed by the .docx and .odt examples, which ExamplesTest runs. + extensions: dom, simplexml, zip - name: Install Composer Dependencies uses: ramsey/composer-install@v4 diff --git a/.gitignore b/.gitignore index 32df0b84..ce1115a4 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ vendor/ composer.lock .phpunit.result.cache .idea/ +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 46c7f600..8e390a22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,15 @@ QueryPath Changelog # Unreleased changes -- +- Reorganise, modernise, and repair the `examples/` directory. Each example now lives in its own subdirectory with an `index.php`, and the full set is indexed in `examples/quickstart-guide.md` +- Convert the remaining legacy examples: `simple_example.php`, `techniques.php`, `svg.php`, `rss.php`, `odt.php`, `parse_php.php`, and `sparql.php` +- Fix examples that no longer ran: send a `User-Agent` where remote hosts now require one, resolve paths relative to the example rather than the working directory, and stop relying on the removed `qp.php` autoloader and the PHP 8 incompatible `eachLambda()` +- Rewrite `examples/quickstart-guide.md`, which documented an autoloader and a set of Composer caveats that no longer apply +- Remove unused example fixtures (`The_Beatles.rdf`, `testGrid.html`, `out.svg`) and generated output +- Add `ext-dom` and `ext-simplexml` to the `composer.json` requirements, and `ext-zip` to the dev requirements +- Add `QueryPathTests\ExamplesTest`, which runs every offline example on each supported PHP version and fails if one stops working. The examples that call third-party services are run by the new `Examples` workflow, weekly and whenever an example changes +- Rewrite the cURL example against the PubMed E-utilities API. MusicBrainz throttles by IP address, which made the example unusable from any shared address +- Add `composer run test:examples` (and `test:examples:network`) to run the examples locally # 4.1.0 diff --git a/composer.json b/composer.json index 4d4b236a..73af5c06 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,9 @@ ], "require": { "php": "^7.1 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", - "masterminds/html5": "^2.0" + "masterminds/html5": "^2.0", + "ext-dom": "*", + "ext-simplexml": "*" }, "autoload": { "psr-4": { @@ -36,6 +38,7 @@ ] }, "require-dev": { + "ext-zip": "*", "mockery/mockery": "^1.1", "yoast/phpunit-polyfills": "^1.0", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", @@ -49,7 +52,10 @@ "scripts": { "lint": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs", "lint:fix": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf", - "lint:min-php": "@lint --standard=phpcompat.xml" + "lint:min-php": "@lint --standard=phpcompat.xml", + "test": "@php ./vendor/bin/phpunit", + "test:examples": "@php ./tests/run-examples.php --all", + "test:examples:network": "@php ./tests/run-examples.php --network" }, "replace": { "arthurkushman/query-path": "3.1.4", diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..9aef54a2 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,35 @@ +# QueryPath Examples + +Runnable examples covering the QueryPath API. Each one lives in its own directory +with an `index.php` you can run directly: + +```bash +composer install +php examples/hello-world/index.php +``` + +Most of them print HTML, so they also work if you point a web server at this +directory: + +```bash +php -S localhost:8000 -t examples +``` + +**[Read the QuickStart guide](quickstart-guide.md)** — it introduces the library +and indexes every example in this directory with a note on what each one covers. + +New to QueryPath? Start with [hello-world](hello-world/index.php). + +## Checking they all still work + +The offline examples are part of the unit test suite, so they run on every pull +request across all supported PHP versions. To run the whole set — including the +ones that call third-party services — by hand: + +```bash +composer run test:examples # all of them +composer run test:examples:network # only the ones that need a remote service +``` + +An example passes if it exits cleanly, emits no PHP diagnostic, and produces a +reasonable amount of output. diff --git a/examples/The_Beatles.rdf b/examples/The_Beatles.rdf deleted file mode 100644 index e857d3eb..00000000 --- a/examples/The_Beatles.rdf +++ /dev/null @@ -1,567 +0,0 @@ - - - - - - - Iwanttoholdyourhandsample.ogg - - - - - - - - - - - - I Beatles sono stati un gruppo musicale britannico, originario di Liverpool e in attività dal 1962 al 1970. Hanno segnato un'epoca non solo nella musica ma anche nel costume, nella moda e nella moderna pop art. Considerati uno dei maggiori fenomeni della musica contemporanea, a distanza di vari decenni dal loro scioglimento ufficiale - e dopo la morte di due dei quattro componenti - i Beatles contano ancora su un vasto seguito. I loro lavori sono regolarmente commercializzati in versione digitale, ed arricchiti dal recupero di materiale inedito. Secondo la EMI, la casa discografica che tra il 1986 e il 1987 ne ha recuperato i diritti, le riedizioni dei loro dischi hanno venduto oltre un miliardo di copie. Per la rivista Rolling Stone, i Beatles rappresentano il gruppo musicale più importante ed influente del XX secolo Per il critico musicale Piero Scaruffi, i Beatles appartengono certamente alla storia del costume degli anni '60, ma i loro meriti musicali sono quantomeno dubbi . Numerosi sono i loro fan club, esistenti in ogni parte del mondo. Inoltre, l'aura - per molti versi non sempre codificabile secondo canoni comuni - che circonda lo sviluppo del loro successo a livello mediatico, e lo straordinario esito artistico raggiunto come musicisti-rock, sono tuttora oggetto di studio da parte di persone appassionate o estranee al mondo della musica. - - - The Beatles - - - - - - The Beatles - - - - - - - - - - - - "Strawberry Fields Forever" - - - - - - - - - The Beatles - - - The Beatles - - - - - - group_or_band - - - - - - - - - "A Day in the Life" - - - - - - - - - - - - - - - The Beatles were a pop and rock band from Liverpool, England formed in 1960. Primarily consisting of John Lennon (rhythm guitar, vocals), Paul McCartney, George Harrison and Ringo Starr throughout their career, The Beatles are recognised for leading the mid-1960s musical "British Invasion" into the United States. Although their initial musical style was rooted in 1950s rock and roll and homegrown skiffle, the group explored genres ranging from Tin Pan Alley to psychedelic rock. Their clothes, styles, and statements made them trend-setters, while their growing social awareness saw their influence extend into the social and cultural revolutions of the 1960s. After the band broke up in 1970, all four members embarked upon solo careers. The Beatles are one of the most commercially successful and critically acclaimed bands in the history of popular music, selling over a billion records internationally. In the United Kingdom, The Beatles released more than 40 different singles, albums, and EPs that reached number one, earning more number one albums than any other group in UK chart history. This commercial success was repeated in many other countries; their record company, EMI, estimated that by 1985 they had sold over one billion records worldwide. According to the Recording Industry Association of America, The Beatles have sold more albums in the United States than any other band. In 2004, Rolling Stone magazine ranked The Beatles number one on its list of 100 Greatest Artists of All Time. According to that same magazine, The Beatles' innovative music and cultural impact helped define the 1960s, and their influence on pop culture is still evident today. In 2008, Billboard magazine released a list of top-selling Hot 100 artists to celebrate the chart's fiftieth anniversary; The Beatles reached #1 again. - - - - - - - - - - - - The Beatles - - - ビートルズ - - - - - - - - - "Help!" - - - - - - - - - - - - - - - Getbacksample.ogg - - - - - - Sample of "Help!". - - - 220 - - - - - - - - - - - - - - - - - - - - - - - - - - - The Beatles foi uma banda de rock de Liverpool, Inglaterra com suas raízes no final da década de 1950. A banda é reconhecida por ter liderado a invasão do rock inglês nos Estados Unidos no início dos anos 1960. O grupo foi formado por John Lennon (guitarra, baixo, teclado, gaita, percussão, efeito sonoro e vocal), Paul McCartney, George Harrison e Ringo Starr .O grupo obteve uma fama, popularidade e notoriedade até hoje inéditas para uma banda musical, e tornou-se a banda de maior sucesso e de maior influência do século XX. Os "garotos de Liverpool", como eram chamados, não tiveram apenas impacto sobre a música, mas também influenciaram as vestimentas, os cortes de cabelo e a forma de ser dos jovens daquela geração. Foi esse estrondoso sucesso que inspirou a criação do termo beatlemania. Suas roupas, estilos, e declarações os fizeram líderes da moda para a juventude, enquanto a percepção social do conjunto que crescia viu sua influência se estender na revolução social e cultural dos anos 1960. Atingiram o primeiro lugar nas paradas de sucesso no mundo inteiro com composições próprias como "She Loves You", "Something", "I Want to Hold Your Hand", "Can't Buy Me Love", "Help!", "Yesterday", "Eleanor Rigby", "Hey Jude", "All You Need Is Love", "Let It Be" e "Strawberry Fields Forever", entre outras. Considerado o grupo musical mais bem-sucedido da história, sendo os seus membros aclamados por público e crítica, com mais de um 1,5 bilhão de álbuns vendidos em todo o mundo, e com vinte canções que atingiram o primeiro lugar nas paradas apenas nos Estados Unidos da América, além de conseguirem ocupar em determinado momento os cinco primeiros lugares em meados de 1964 - números recordes até os dias atuais. Pela inventiva criatividade, originalidade e magia de suas canções, John Lennon e Paul McCartney são considerados a maior dupla de compositores da música popular em todo o mundo. Também foram os precursores da música indiana e oriental no pop/rock ocidental, sobretudo pela influência de George Harrison nas composições e instrumentos do grupo, em canções como "Within You Without You", "Norwegian Wood" e "Love You Too".Além de toda a repercussão, os Beatles fizeram todo um modo de criar música que influenciou não só sua época, mas todas as épocas seguintes: foram a primeira banda do planeta a fazer vídeos musicais de suas canções, e o álbum Sgt. Pepper's Lonely Hearts Club Band foi o primeiro do mundo a conter um encarte com fotos e letras de suas canções . Em 2003, a revista especializada em música Rolling Stone classificou Sgt. Pepper's como o melhor álbum de todos os tempos . - - - - - - - - - - - - - - - - - - - - - - - - - - - The Beatles - - - - - - - - - Beatles day life.ogg - - - - - - - - - - - - The Beatles on vuosina 1960–1970 toiminut kaikkien aikojen menestynein musiikkiyhtye. Sen äänitteiden myynti on ylittänyt miljardin kappaleen rajan.. Yhtye on Yhdysvaltojen myydyin. . The Beatles aloitti harmonisten vokaaliosuuksien leimaamalla perinteisellä "beat-musiikilla". Vuoteen 1967 mennessä yhtye oli siirtynyt psykedeelisen rockin ja kokeellisten sovitusten kautta huomattavasti monimutkaisempaan ilmaisuun. Beatlesistä tuli populaarimusiikin keskeisimpiä suunnannäyttäjiä. Se on säilyttänyt suuren suosionsa myös vuonna 1970 tapahtuneen hajoamisensa jälkeen. Ennen The Beatlesia laulusolistit olivat hallinneet pop-musiikkia, mutta The Beatlesin läpimurron jälkeen yhtyeet ja niiden jäsenet saivat laulusolisteja vastaavan suosion yleisön keskuudessa. The Beatlesilla oli myös suuri vaikutus siihen, että muusikot alkoivat enemmän itse tehdä musiikkiaan, ja että pop-musiikkia alettiin pitää taiteen lajina. Musiikin uudistamisen kannalta merkittävimpänä levynä on pidetty albumia Sgt. Pepper's Lonely Hearts Club Band (1967). Beatlesin parhaaksi levyksi on ehdotettu tämän ohella myös Rubber Soulia, Revolveria, Abbey Roadia tai The Beatlesia (tunnetaan myös nimellä "The White Album" tai "Valkoinen tupla"). - - - - - - The Beatles var en af de mest indflydelsesrige musikgrupper i 1960'erne. Udover at være en kommerciel kæmpesucces var gruppen også stilsættende hvad angår design, markedsføring og rent musikalsk. Gruppen var fire drenge fra Liverpool, der både var venner, men også konkurrenter, der bestandigt forsøgte at overgå hinanden. Kernen i gruppen var komponisterne Lennon & McCartney, der sammen og hver for sig skrev en lang stribe hitsange, som næsten alle blev indspillet af gruppen selv. Gang på gang op igennem deres karriere satte gruppen nye rekorder for, hvad der var opnåeligt indenfor populærmusikken. Gruppen blev i starten betragtet som et teenagefænomen med vildt hår og skrigende fans. Deres turnéer blev temmeligt kaotiske pga. horder af fans som blokerede lufthavne og gader, når gruppen besøgte en by. 1963 til 1966, hvor gruppen stoppede sin turnevirksomhed, var højdepunktet i Beatlemania (Beatlegalskaben), som medierne døbte fænomenet. The Beatles havde sit udgangspunkt i Mersey-beaten, med dens specielle rytme og lyd inspireret af sømændenes import af amerikanske soul-plader. Da gruppen flyttede til London for at indspille plader og derefter fik sit nationale og internationale gennembrud, blev de i høj grad eksponenter for British Invasion genren. Fra udkantsbyen Liverpool lykkedes det gruppen at erobre verden i bogstavelig forstand. Deres musik har gået sin sejrsgang verden over og influeret på, hvordan populærmusik fortolkes. Gruppen har leveret noget af den mest kopierede musik, der er skrevet. Heriblandt sangen "Yesterday", som er den mest kopierede sang nogensinde. Kategori:Kilder manglerGruppen var meget eksperimenterende i sit udtryk, men samtidig i stand til at gøre de mest rabiate nyskabelser acceptable inden for populærmusikken. Gruppen startede med rå beatmusik, for så at kombinere den hårde rock med bløde ballader, elektriske instrumenter med klassiske arrangementer, indiske instrumenter og båndklip spillet forlæns, baglæns og som tilfældigt sammenklippede lydfragmenter. - - - - - - - - - - - - The Beatles - - - - - - Pop, rock and various others - - - - - - - - - - - - The Beatles foi uma banda de rock de Liverpool, Inglaterra com suas raízes no final da década de 1950. - - - The Beatles var et rockeband fra Liverpool som ble dannet i 1959. De debuterte på plate i 1962 med singelen «Love Me Do» som hadde moderat suksess. - - - - - - - - - The Beatles - - - - - - 1960–1970, 1994–1995 - - - - - - group_or_band - - - The Beatles bootlegs - - - The Beatles var en af de mest indflydelsesrige musikgrupper i 1960'erne. Udover at være en kommerciel kæmpesucces var gruppen også stilsættende hvad angår design, markedsføring og rent musikalsk. - - - - - - - - - - - - - - - - - - The Beatles were a pop and rock band from Liverpool, England formed in 1960. Primarily consisting of John Lennon (rhythm guitar, vocals), Paul McCartney, George Harrison and Ringo Starr throughout their career, The Beatles are recognised for leading the mid-1960s musical "British Invasion" into the United States. Although their initial musical style was rooted in 1950s rock and roll and homegrown skiffle, the group explored genres ranging from Tin Pan Alley to psychedelic rock. Their clothes, styles, and statements made them trend-setters, while their growing social awareness saw their influence extend into the social and cultural revolutions of the 1960s. After the band broke up in 1970, all four members embarked upon solo careers. - - - The Beatles - - - - - - - - - - - - The Beatles fue un grupo musical inglés de pop rock de la década de los 60 que se formó en la ciudad de Liverpool, Inglaterra en 1957. - - - I Beatles sono stati un gruppo musicale britannico, originario di Liverpool e in attivit� dal 1962 al 1970. - - - The Beatles - - - - - - Sample of "Strawberry Fields Forever". - - - - - - The Beatles - - - - - - - - - The Beatles var et rockeband fra Liverpool som ble dannet i 1959. De debuterte på plate i 1962 med singelen «Love Me Do» som hadde moderat suksess. Det store gjennombruddet i Storbritannia kom i 1963 med gruppens andre singel, «Please Please Me», som gikk til topps på de engelske hitlistene. Gjennombruddet i De forente stater kom i februar 1964, med singelen «I Want to Hold Your Hand». Gruppen forble en av de fremste populærmusikkgruppene gjennom hele 1960-tallet, frem til den ble offisielt oppløst i 1970. De hadde da dominert hitlistene og musikkbransjen på begge sider av Atlanteren gjennom hele karrieren, og er fremdeles historiens mestselgende plateartister. Kategori:Artikler som trenger referanser De påvirket etterkrigstidens babyboom-generasjon i Storbritannia, De forente stater og mange andre land i 1960-årene. De er utvilsomt den mest populære gruppen i rockehistorien, med over 1,1 milliard solgte plater verden over. Kategori:Artikler som trenger referanser Selv om de helt i begynnelsen var kjent for en variant av lett popmusikk som ble kalt merseybeat, ble deres senere verker mottatt med en popularitet og kritisk hyllest som kanskje overgår alt i det 20. århundre. De ble mer enn kun plateartister, og påvirket moter og kultur, med ringvirkninger til film og politisk aktivisme. De oppnådde en ikonstatus som ga dem enorm påvirkningskraft. - - - - - - - - - - - - - - - "Get Back" - - - - - - - - - the+beatles - - - Sample of "A Day in the Life", a track appearing on Sgt. Pepper. - - - Sample of "I Want to Hold Your Hand". - - - - - - - - - - - - - - - - - - The Beatles fue un grupo musical inglés de pop rock de la década de los 60 que se formó en la ciudad de Liverpool, Inglaterra en 1957. Llamada anteriormente "The Quarrymen" y se disolvió en abril de 1970, que lideró la "Invasión británica" en los Estados Unidos.Es una de las bandas más exitosas y críticamente aclamadas de la historia de la música popular. En el Reino Unido lanzaron más de 40 diferentes singles, álbumes y EP que alcanzaron el número uno, éxito comercial que se repitió en muchos otros países. Se estima que han vendido más de mil millones de discos alrededor del mundo, según su casa de discos EMI, además de ser los artistas que más discos han vendido en la historia de los Estados Unidos, de acuerdo con la Recording Industry Association of America.Se caracterizaban en un principio por su rock and roll con raíces en los 50s, pero que con el transcurso de su carrera fueron experimentando con muchos otros géneros desde el Tin Pan Alley hasta el rock psicodélico. Su forma de vestirse, su estilo y sus declaraciones, tuvieron una inmensa influencia en la sociedad, revolucionando la cultura de la década de los 60s.En 1988, fueron los colocó en el #1 dentro de su lista de 100 Greatest Artists of All Time. De acuerdo con la misma publicación, ellos innovaron la música y su impacto cultural ayudó a definir los años 1960 y su influencia en la cultura pop aún es evidente en la actualidad, además de tener 4 álbumes entre los 10 mejores de la historia, incluyendo el 1º y el 3º. Unos años más tarde también fueron colocados en el puesto nº 1 entre Los Mejores Artistas de todos los tiempos por la página de internet de música Acclaimedmusic. net. Cite error: Invalid <ref> tag; refs with no name must have content - - - The Beatles record sales, worldwide charts - - - - - - - - - "I Want to Hold Your Hand" - - - The Beatles - - - The Beatles - - - - - - - - - - - - - - - The Beatles on vuosina 1960–1970 toiminut kaikkien aikojen menestynein musiikkiyhtye. Sen äänitteiden myynti on ylittänyt miljardin kappaleen rajan.. Yhtye on Yhdysvaltojen myydyin. - - - - - - 2008 October 2 - - - - - - - - - - - - - - - - - - The Beatles var en stilbildande brittisk pop- och rockgrupp under 1960-talet. Gruppen bestod av fyra medlemmar: John Lennon (1940-1980), Paul McCartney, George Harrison och Ringo Starr (född 1940). Samtliga medlemmar kom från Liverpool i Storbritannien. The Beatles upplöstes 1970 men är fortfarande en av världens populäraste popgrupper. Deras skivor och låtar brukar toppa omröstningar när kritiker och lyssnare röstar om världens bästa skiva eller låt. - - - The Beatles waren die bekannteste englische Band der Beatmusik. Die Band formierte sich in Liverpool. Die Gruppe gilt mit bisher ca. 1,3 Milliarden verkauften Tonträgern als die erfolgreichste und einflussreichste Band des 20. Jahrhunderts. Die erste Schallplatte der Beatles erschien im Jahr 1962. Künstlerische und persönliche Differenzen führten 1970 zur Trennung der Gruppe. - - - - - - - - - - - - - - - - - - The Beatles - - - The Beatles - - - - - - The Beatles - - - - - - - - - - - - - - - Strawberryfields.ogg - - - - - - on - - - - - - - - - - - - - - - The Beatles - - - Paul McCartney, John Lennon, George Harrison, Ringo Starr and The Beatles - - - - - - - - - - - - - - - The Beatles («Битлз»; отдельно участников ансамбля называют «битлами», также «битлзами») — британская рок-группа, внёсшая большой вклад в развитие рок-музыки. Aнсамбль не только изменил её, но и достиг беспрецедентной популярности, благодаря чему The Beatles стали одним из ярких феноменов мировой культуры 60-х гг. XX века. - - - Sample of "Get Back". - - - The Beatles («Битлз»; отдельно участников ансамбля называют «битлами», также «битлзами») — британская рок-группа, внёсшая большой вклад в развитие рок-музыки. - - - - - - - - - - - - List of Beatles songs - - - - - - 披頭四樂隊(英文:The Beatles,Beatles的意思是「甲虫」,所以又譯「甲壳虫樂隊」)来自英国利物浦的著名四人樂隊組合。他們在1962年与EMI百代唱片公司签约录制唱片。1963年初,单曲唱片《Please Please Me》登上英国排行榜首位。1964年,披头四首次前往美国演出。1966年在东京的日本武道馆举行了大规模的音乐会,1966年8月29日晚在旧金山举行最后一场收费现场音乐会。1970年4月10日,保羅·麥卡尼个人专辑中的话被媒体視為宣布樂隊解散,1970年12月31日,保罗正式起诉乐队其他三位成员,要求结束乐队合作并指明管理并分配乐队财产的正式人选。 披頭四至今已在全球售出至少7億張唱片(EMI方面宣布已超過10億張),是人類音樂史上前三暢銷的歌手。 - - - - - - The Beatles var en stilbildande brittisk pop- och rockgrupp under 1960-talet. - - - Help!.ogg - - - - - - 披頭四樂隊 - - - 披� �四樂隊(英文:The Beatles,Beatles的意思是「甲虫」,所以又譯「甲壳虫樂隊」)来自英国利物浦的著名四人樂隊組合。他們在1962年与EMI百代唱片公司签约录制唱片。1963年初,单曲唱片《Please Please Me》登上英国排行榜首位。1964年,披头四首次前往美国演出。1966年在东京的日本武道馆举行了大规模的音乐会,1966年8月29日晚在旧金山举行最后一场收费现场音乐会。1970年4月10日,保羅·麥卡尼个人专辑中的话被媒体視為宣布樂隊解散,1970年12月31日,保罗正式起诉乐队其他三位成员,要求结束乐队合作并指明管理并分配乐队财产的正式人选。 披� �四至今已在全球售出至少7億張唱片(EMI方面宣布已超過10億張),是人類音樂史上前三暢銷的歌手。 - - - - - diff --git a/examples/at_a_glance.php b/examples/at_a_glance.php deleted file mode 100644 index 5f95a5f2..00000000 --- a/examples/at_a_glance.php +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - -
onetwothree
fourfivesix
-EOF; - -print "\nExample 1: \n"; -// Get all of the elements in the document and add the -// attribute `foo='bar'`: -qp($xml, 'td')->attr('foo', 'bar')->writeXML(); - -print "\nExample 2: \n"; - -// Or print the contents of the third TD in the second row: -print qp($xml, '#row2>td:nth(3)')->text(); - -print "\nExample 3: \n"; -// Or append another row to the XML and then write the -// result to standard output: -qp($xml, 'tr:last')->after('')->writeXML(); diff --git a/examples/test.docx b/examples/basic-docx-parser/example.docx similarity index 100% rename from examples/test.docx rename to examples/basic-docx-parser/example.docx diff --git a/examples/basic-docx-parser/example.xml b/examples/basic-docx-parser/example.xml new file mode 100644 index 00000000..d7341169 --- /dev/null +++ b/examples/basic-docx-parser/example.xml @@ -0,0 +1,2035 @@ + + + + + + + + + + + + + + + + + + + + + + Computer Forensics Class Syllabus + + + + + + + + + - + + + + + + + + + + Fall + + + + + + + + + + 2009 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Course No.: + + + + + + + + + Comp 340-001 / + + + + + + + + Comp 488-004 + + + + + + + + + + + + + + + + + + + + + + + + + + + INSTRUCTOR: + + + + + + + + + + + + + + + + + Scott Jones + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Email + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + sjone18@luc.edu + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Work Phone + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 312-915-7987 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Office Location + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 512D Lewis Tower + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Office Hours + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 6PM to 7PM Wednesdays or by appt. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OFFICIAL TEXT: + + + + + + + + + + + + + + + + + + + + + + + “Computer Evidence Collection and Preservation Second Edition” + + + + + + + + + + + + + + + + + by + + + + + + + + Christopher L.T. Brown, ISBN-10: 1-58450-699-7 + + + + + + + + + + + + + + + + + + + + + + + + + The schedule of class readings is officially as follows: + + + + + + + + + + + + + + + + + + + + + + + + + Week + + + + + + + + + + + + + + + Date + + + + + + + + + + + + + + + Book Chapter To Read + + + + + + + + + + + + + + + + ------------------------ + + + + + + + ----------------------------------------------------- + + + + + + + + + + + + + + + + + + + + + 8/26 + + + + + + + + + + + + + + N/A – Class Intro + + + + + + + + + + + + + + + + + + + + + 9/2 + + + + + + + + + + + + + + + Chapters 1 & 2 + + + + + + + + + + + + + + + + + + + + + 9/9 + + + + + + + + + + + + + + + Chapters 3 & 4 + + + + + + + + + + + + + + + + + + + + + 9/16 + + + + + + + + Chapters 5 & 6 + + + + + + + + + + + + + + + + + + + + + 9/23 + + + + + + + + Chapters 7 & 8 + + + + + + + + + + + + + + + + + + + + + 9/30 + + + + + + + + Chapters 9 & 10 + + + + + + + + + + + + + + + + + + + + + 10/7 + + + + + + + + Chapters 11 & 12 + + + + + + + + + + + + + + + + + + + + + 10/14 + + + + + + + + Chapters 13 & 14 + + + + + + + + + + + + + + + + + + + + + 10/21 + + + + + + + + Chapters 15, 16, & 17 + + + + + + + + + + + + + + + + + + + + + 10/28 + + + + + + + + In-class projects + + + + + + + + + + + + + + + + + + + + + 11/4 + + + + + + + + In-class projects + + + + + + + + + + + + + + + + + + + + + 11/11 + + + + + + + + In-class projects + + + + + + + + + + + + + + + + + + + + + 11/18 + + + + + + + + In-class projects + + + + + + + + + + + + + + + + + + + + + 11/25 + + + + + + + + In-class projects + + + + + + + * + + + + + + + + + + + + + + + + + + + + + 12/2 + + + + + + + + In-class projects + + + + + + + + + + + + + + + + + + + + + 12/9 + + + + + + + + In-class projects + + + + + + + * + + + + + + + + + + + + + + + + + + + + + 12/16 + + + + + + + + In-class projects + + + + + + + * + + + + + + + + + + + + + + + + + + + + + + + + + + + Class readings and assignments may change as needed. + + + + + + + 12/9/09 is generally designated a rest/study day in the academic calendar, so class may not be + meeting. 12/16/09 is the final Wednesday within this semester and the tentative date for the final + exam. This may be changed with short notice by the school as needed. + + + + + + + + To the extent that a given date for class is cancelled by the school for any reason, that week’s reading will be bumped back accordingly. + + + + + + + + + + + + + + + + + + + + + + + + + As newsworthy events become available, we may supplement our classroom discussions accordingly. I + encourage all students to pay attention to news stories on television or the Internet related to + computer forensics and computer technology in general. + + + + + + + + + + + + + + + + + + + + + + + + + + + + IN-CLASS PROJECTS: + + + + + + + + Starting in week four, this class will begin to conduct in-class work using real-world computer + forensics hardware and/or software. In order to accomplish this, I highly recommend that each + student bring two or more USB thumb drives to class. I recommend that the smaller of the two be + around 512 MB in size and the larger one be at least 2 GB in size. + + + + + + + + + + + + + + + + + + + + + + + + + + + + QUIZZES & TESTS: + + + + + + + + All quizzes and tests will be administered via Blackboard. Without prior arrangements I will not + accept make up quizzes or tests. Emergencies require that I receive an email in a timely fashion (24 + hour period) telling me your name, the date you missed class, and explaining to me why I should + allow you to take the quiz or test late. By default my answer will be no. If I allow you to retake + the quiz or test, I reserve the right to deduct points accordingly for your lateness. + + + + + + + + I reserve the right to give pop quizzes at any time. + + + + + + + + + + + + + + + + + + + + + + + + + + + GROUPWORK: + + + + + + + + + + + + + + + Each student will be sharing a laptop computer with at least one other student each week. I may + also require students + + + + + + + + to work in larger groups for specific tasks/projects. I reserve the right to ask specific students to swap groups if/when I find that students are not intermingling adequately. In the real world, you do not always get to choose with whom you work, and this class strives to provide real world examples and experience. + + + + + + + + + + + + + + + + + + + + + + + + + + + GOALS: + + + + + + + + + + + + + + + There are a few specific goals for which this class will strive. + + + + + + + + + + + + + + + + + GOAL 1 – by mid-term, every student will have created at least one or more forensic images + + + + + + + + + + + + + + + + + GOAL 2 – every student understands how to create basic forensic images + + + + + + + + + + + + + + + + + GOAL 3 – every student understands the fundamental theories underlying computer forensics + + + + + + + + + + + + + + + + + GOAL 4 – every student will gain basic skills desired presently by employers for IT staff + + + + + + + + + + + + + + + + + + + + + + + + + + + GRADE SCALE: + + + + + + + + Every student EARNS their own grade. The grading scale is as follows: + + + + + + + + + + + + + + + + + + + + + + + + + + 92+ + + + + + + + + + + + + + + + A + + + + + + + + + + + + + + + + + 91 + + + + + + + + + + + + + + + A- + + + + + + + + + + + + + + + + + 90 + + + + + + + + + + + + + + + B+ + + + + + + + + + + + + + + + + + 89 – 84 + + + + + + + + + + + + + + + B + + + + + + + + + + + + + + + + + 83 + + + + + + + + + + + + + + + B- + + + + + + + + + + + + + + + + + 82 + + + + + + + + + + + + + + + C+ + + + + + + + + + + + + + + + + + 81 + + + + + + + -75 + + + + + + + + + + + + + + + C + + + + + + + + + + + + + + + + + 74 + + + + + + + + + + + + + + + C- + + + + + + + + + + + + + + + + + 73 + + + + + + + + + + + + + + + D+ + + + + + + + + + + + + + + + + + 72-67 + + + + + + + + + + + + + + + D + + + + + + + + + + + + + + + + + 66 + + + + + + + + + + + + + + + D- + + + + + + + + + + + + + + + + + 65 or less + + + + + + + + F + + + + + + + + + + + + + + + + + + + + + + + + + + + Instructor Comments: + + + + + + + + There are no dumb questions, only unasked ones. If you do not understand something it is your + responsibility to let me know. Please ask questions in class, during breaks, after class, and even + via email if you like. I will assume you understand and m + + + + + + + + ove on if you do not inform me otherwise + + + + + + + . + + + + + + + + + + + + + + + + + + + + + + + + + I welcome feedback from my students. Please feel free to send an email to the above LUC email address. No class-related emails at any other address will be accepted. + + + + + + + + + + \ No newline at end of file diff --git a/examples/basic-docx-parser/index.php b/examples/basic-docx-parser/index.php new file mode 100644 index 00000000..26cb440e --- /dev/null +++ b/examples/basic-docx-parser/index.php @@ -0,0 +1,137 @@ + a paragraph + * a run - a span of text sharing one set of formatting + * the run's formatting ( for bold, for underline) + * the text itself + * + * A copy of the extracted XML is included as `example.xml` if you want to read + * through it. + * + * @author Emily Brand + * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. + * @see https://www.php.net/manual/en/class.ziparchive.php + */ + +use QueryPath\CSS\ParseException; +use QueryPath\DOMQuery; +use QueryPath\Exception; + +require_once __DIR__ . '/../../vendor/autoload.php'; + +echo '

Create a Basic Docx Parser

'; + +echo '

This example parses example.docx, walks its nodes, and displays the text with basic formatting. example.xml in this directory is the XML extracted from that file - the document QueryPath actually processes.

'; + +echo '

Content of example.docx file...

'; + +try { + // Load the example.docx file, parse for text nodes and output with basic formatting + foreach (qp(docx2text(__DIR__ . '/example.docx'), 'w|p') as $qp) { + /** @var $qp DOMQuery */ + /** @var $qr DOMQuery */ + foreach ($qp->find('w|r') as $qr) { + echo format($qr); + } + + echo '
'; + } +} catch (Exception $e) { + echo $e->getMessage(); + exit(1); +} + +/** + * Get the node text and apply basic formatting, if necessary + * + * @param DOMQuery $qp + * + * @return string + * @throws ParseException + * @throws Exception + */ +function format(DOMQuery $qp): string +{ + $text = $qp->find('w|t')->text() . ' '; + + $text = checkUnderline($qp) ? sprintf('%s', $text) : $text; + $text = checkBold($qp) ? sprintf('%s', $text) : $text; + + return $text; +} + +/** + * Look for the node to determine if the text is bolded + * + * @param DOMQuery $qp + * + * @return bool + * @throws ParseException + * @throws Exception + */ +function checkBold(DOMQuery $qp): bool +{ + return (bool) $qp->children('w|rPr') + ->children('w|b') + ->count(); +} + +/** + * Look for the node to determine if the text is underlined + * + * @param DOMQuery $qp + * + * @return bool + * @throws ParseException + * @throws Exception + */ +function checkUnderline(DOMQuery $qp): bool +{ + return (bool) $qp->children('w|rPr') + ->children('w|u') + ->count(); +} + +/** + * Extract the text from a docx file + * + * @param string $archiveFile The path to the .docx file to extract information from + * @return string + */ +function docx2text(string $archiveFile): string +{ + $dataFile = 'word/document.xml'; + + if (!class_exists('ZipArchive', false)) { + throw new RuntimeException('ZipArchive extension must be enabled to parse .docx files'); + } + + $zip = new ZipArchive(); + // Open received archive file + if (true !== $zip->open($archiveFile)) { + throw new RuntimeException('Could not open the file using ZipArchive: ' . $zip->getStatusString()); + } + + $data = ''; + // Search for the docx data file + if (($index = $zip->locateName($dataFile)) !== false) { + $data = $zip->getFromIndex($index); + } + + // Close zip to prevent memory leak + $zip->close(); + + return $data; +} diff --git a/examples/basic-manipulation-filter-and-retrieval/index.php b/examples/basic-manipulation-filter-and-retrieval/index.php new file mode 100644 index 00000000..3a44296f --- /dev/null +++ b/examples/basic-manipulation-filter-and-retrieval/index.php @@ -0,0 +1,208 @@ + block. Use writeHTML(), + * writeHTML5(), or writeXML() instead when you want the document written + * straight to the output buffer. + */ + +/* + * HTML Example + */ +$html = << + + one + two + three + + + + four + five + six + + +EOF; + +/* + * XML Example + */ +$xml = << + + + This is the DOM description... + + + + This is the Traversing description... + + + + This is the Filtering description... + + + + This is the Selectors description... + + +EOF; + +try { + echo '

Basic HTML Usage

'; + echo 'The following HTML chunk will get parsed, traverse, filtered, and manipulated:'; + echo '
' . htmlspecialchars($html) . '
'; + + echo '

Example 1

'; + echo 'Add the attribute class="cell" to all <td> elements:'; + + echo '
<?php 
+
+echo html5qp($html, "td")
+->attr("class", "cell")
+->parents("table")
+->html()  
+
'; + + echo 'This will output the following HTML:'; + + echo '
';
+
+	echo htmlspecialchars(
+		html5qp($html, 'td')
+			->attr('class', 'cell')
+			->parents('table') // traverse up the DOM until we match the table
+			->html() // get the HTML of the table
+	);
+
+	echo '
'; + + echo 'If you want to output a valid HTML document, replace parents(\'table\') with top():'; + + echo '
';
+
+	echo htmlspecialchars(
+		html5qp($html, 'td')
+			->attr('class', 'cell')
+			->top()
+			->html()
+	);
+
+	echo '
'; + + echo '

Example 2

'; + echo 'Find and output the text of the second cell in the second row of the table:'; + + $text = html5qp($html) + ->find('#row2 > td:nth-child(2)') + ->text(); + + echo '
<?php 
+
+echo html5qp($html)
+->find("#row2 > td:nth-child(2)")
+->text();
+
+// Result: '. $text. '
+
'; + + echo '

Example 3

'; + echo 'Append an additional row at the end of the table:'; + echo '
<?php 
+
+echo html5qp($html, "td")
+->after("<tr><td>seven</td><td>eight</td><td>nine</td></tr>")
+->parents("table") // traverse up the DOM until we match the table
+->html()
+
'; + + echo 'This will output the following HTML:'; + + echo '
';
+
+	echo htmlspecialchars(
+		html5qp($html, 'tr:last')
+			->after("\n\n\t\n\t\tseven\n\t\teight\n\t\tnine\n\t")
+			->parents('table')
+			->html()
+	);
+
+	echo '
'; + + echo '

Basic XML Usage

'; + echo 'The following XML will get parsed, traverse, filtered, and manipulated:'; + echo '
' . htmlspecialchars($xml) . '
'; + + echo '

Example 1

'; + echo 'Add the attribute class="item" to all <desc> elements:'; + + echo '
<?php 
+
+echo qp($xml, "desc")
+->attr("class", "item)
+->top() // return to the root node (<categories>)
+->xml(); // output a valid XML document.
+
'; + + echo 'This will output the following XML:'; + + echo '
';
+
+	echo htmlspecialchars(
+		qp($xml, 'desc')
+			->attr('class', 'item')
+			->top() // return to the root node
+			->xml() // output a valid XML document
+	);
+
+	echo '
'; + + echo 'You can omit the XML declaration by setting the first argument to true: ->xml(true).'; + + echo '

Example 2

'; + echo 'Find and output the text of the third <desc> tag:'; + + $text = qp($xml) + ->find('categories > category:nth-child(3) desc') + ->text(); + + echo '
<?php 
+
+echo qp($xml)
+->find("categories > category:nth-child(3) desc")
+->text();
+ 
+ // Result: '.$text.'
+
'; + + echo '

Example 3

'; + echo 'Append a category at the end of the group:'; + echo '
<?php 
+
+echo qp($xml, "category:last")
+->after("<category name=\'Appended\'><desc>The appended node...</desc></category>")
+->top()
+->xml()
+
'; + + echo 'This will output the following HTML:'; + + echo '
';
+
+	echo htmlspecialchars(
+		qp($xml, 'category:last')
+			->after("\n\n\t\n\t\tThe appended node...\n\t")
+			->top()
+			->xml()
+	);
+
+	echo '
'; +} catch (\QueryPath\Exception $e) { + // Handle QueryPath exceptions + echo $e->getMessage(); + exit(1); +} diff --git a/examples/openoffice.odt b/examples/basic-odt-parser/example.odt similarity index 100% rename from examples/openoffice.odt rename to examples/basic-odt-parser/example.odt diff --git a/examples/basic-odt-parser/index.php b/examples/basic-odt-parser/index.php new file mode 100644 index 00000000..aa78af71 --- /dev/null +++ b/examples/basic-odt-parser/index.php @@ -0,0 +1,98 @@ + + * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. + * @see https://www.php.net/manual/en/wrappers.compression.php + */ + +require_once __DIR__ . '/../../vendor/autoload.php'; + +echo '

Parsing an Open Document Text file

'; + +echo '

This example reads example.odt and rebuilds its outline, bullet list, and ordered list.

'; + +try { + /* + * Point QueryPath at content.xml inside the ZIP archive. + * + * The zip:// wrapper takes the form zip://# + */ + $doc = qp('zip://' . __DIR__ . '/example.odt#content.xml'); + + /* + * Build the document outline. + * + * Every heading is a element, and its depth is recorded in the + * text:outline-level attribute. Namespaced attributes use the same pipe + * syntax as elements. + */ + echo '

Outline

'; + echo '
';
+
+	foreach ($doc->find('text|h') as $header) {
+		$level = (int) $header->attr('text:outline-level');
+
+		echo str_repeat('    ', max($level - 1, 0)) . '- ' . htmlspecialchars($header->text()) . PHP_EOL;
+	}
+
+	echo '
'; + + /* + * ODT does not mark up bullet and numbered lists differently - both are a + * . What separates them is the list style applied to it, so we + * match on the text:style-name attribute. + * + * Each item is a wrapping a . + */ + echo '

Bullet list

'; + echo '
    '; + + foreach ($doc->top()->find('text|list[text|style-name="L1"] text|list-item text|p') as $item) { + echo '
  • ' . htmlspecialchars($item->text()) . '
  • '; + } + + echo '
'; + + echo '

Ordered list

'; + echo '
    '; + + foreach ($doc->top()->find('text|list[text|style-name="L2"] text|list-item text|p') as $item) { + echo '
  1. ' . htmlspecialchars($item->text()) . '
  2. '; + } + + echo '
'; + + /* + * Body copy is stored in elements. Skipping the empty ones keeps + * the blank "spacer" paragraphs a word processor leaves behind out of the way. + */ + echo '

Body copy

'; + + foreach ($doc->top()->find('office|text > text|p') as $paragraph) { + $text = trim($paragraph->text()); + + if ($text === '') { + continue; + } + + echo '

' . htmlspecialchars($text) . '

'; + } +} catch (\QueryPath\Exception $e) { + // Handle QueryPath exceptions + echo $e->getMessage(); + exit(1); +} diff --git a/examples/create-html-document/index.php b/examples/create-html-document/index.php new file mode 100644 index 00000000..98348643 --- /dev/null +++ b/examples/create-html-document/index.php @@ -0,0 +1,124 @@ + + * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. + */ + +require_once __DIR__ . '/../../vendor/autoload.php'; + +echo '

Building a HTML Document with QueryPath

'; + +echo 'You can use QueryPath to build complex HTML documents using a simple jQuery-like API:'; + +/* + * The chain below finishes with html(), which returns the markup as a string so + * it can be escaped and shown inside a
 block. To send the document
+ * straight to the output buffer instead, swap html() for writeHTML() (or
+ * writeHTML5() / writeXML(), depending on the format you want).
+ */
+
+echo '
<?php 
+
+// Begin with an HTML5 stub document and navigate to the title.
+echo html5qp(\QueryPath\QueryPath::HTML5_STUB, "title")
+	// Add text to the title
+	->text("Example of QueryPath.")
+	// Traverse to the root of the document, then locate the body tag
+	->top("body")
+	// Inside the body, add a heading and paragraph.
+	->append("<h1>This is a test page</h1><p>Test text</p>")
+	// Select the paragraph we just created inside the body
+	->children("p")
+	// Add a class attribute to the paragraph
+	->attr("class", "some-class")
+	// And an inline style to the paragraph
+	->css("background-color", "#eee")
+	// Traverse back up the DOM to the body
+	->parent()
+	// Add an empty table to the body, before the heading
+	->prepend("<table id=\'my-table\'></table>")
+	// Now go to the table...
+	->find("#my-table")
+	// Add a couple of empty rows
+	->append("<tr></tr><tr></tr>")
+	// select the rows (both at once)
+	->children()
+	// Add a CSS class to both rows
+	->addClass("table-row")
+	// Get the first row (at position 0)
+	->eq(0)
+	// Add a table header in the first row
+	->append("<th>This is the header</th>")
+	// Now go to the next row
+	->next()
+	// Add some data to this row
+	->append("<td>This is the data</td>")
+	// Traverse to the root of the document
+	->top()
+	// Write it all out as HTML
+	->html();
+';
+
+echo '
'; + +echo '

Results

'; + +try { + echo '
';
+
+	echo htmlspecialchars(
+	// Begin with an HTML5 stub document and navigate to the title.
+		html5qp(\QueryPath\QueryPath::HTML5_STUB, 'title')
+			// Add text to the title
+			->text('Example of QueryPath.')
+			// Traverse to the root of the document, then locate the body tag
+			->top('body')
+			// Inside the body, add a heading and paragraph.
+			->append('

This is a test page

Test text

') + // Select the paragraph we just created inside the body + ->children('p') + // Add a class attribute to the paragraph + ->attr('class', 'some-class') + // And an inline style to the paragraph + ->css('background-color', '#eee') + // Traverse back up the DOM to the body + ->parent() + // Add an empty table to the body, before the heading + ->prepend('
') + // Now let's go to the table... + ->find('#my-table') + // Add a couple of empty rows + ->append('') + // select the rows (both at once) + ->children() + // Add a CSS class to both rows + ->addClass('table-row') + // Get the first row (at position 0) + ->eq(0) + // Add a table header in the first row + ->append('This is the header') + // Now go to the next row + ->next() + // Add some data to this row + ->append('This is the data') + // Traverse to the root of the document + ->top() + // Write it all out as HTML + ->html() + ); + + echo '
'; +} catch (\QueryPath\Exception $e) { + echo $e->getMessage(); + exit(1); +} diff --git a/examples/create-svg-document/index.php b/examples/create-svg-document/index.php new file mode 100644 index 00000000..1c7babae --- /dev/null +++ b/examples/create-svg-document/index.php @@ -0,0 +1,65 @@ + shapes.svg + * + * @author M Butcher + * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. + * @see https://www.w3.org/TR/SVG11/ + */ + +require_once __DIR__ . '/../../vendor/autoload.php'; + +/* + * A minimal SVG document to build on top of. + * + * Like every XML document handled by qp(), it begins with the XML declaration. + */ +$svg_stub = ' + + Created by QueryPath. +'; + +try { + qp($svg_stub) + // The root element is selected by default, so attr() applies to it. + // Passing an array sets several attributes in one call. + ->attr(['width' => 200, 'height' => 120]) + // Add two rectangles to the canvas. + ->append('') + // Select the second rectangle and position it. + ->find('#second') + ->attr(['x' => 60, 'y' => 20, 'width' => 100, 'height' => 80, 'fill' => 'red']) + // prev() steps back to the preceding sibling - the first rectangle. + ->prev() + ->attr(['x' => 20, 'y' => 20, 'width' => 100, 'height' => 80, 'fill' => 'navy']) + // Add a caption. Note that text() escapes its input for you. + ->top() + ->append('') + ->find('#caption') + ->attr(['x' => 20, 'y' => 115, 'font-family' => 'sans-serif', 'font-size' => 12]) + ->text('Drawn with QueryPath') + // writeXML() prints the whole document, starting from the root. + ->top() + ->writeXML(); +} catch (\QueryPath\Exception $e) { + // Handle QueryPath exceptions + echo $e->getMessage(); + exit(1); +} diff --git a/examples/create-xml-document/index.php b/examples/create-xml-document/index.php new file mode 100644 index 00000000..471442f8 --- /dev/null +++ b/examples/create-xml-document/index.php @@ -0,0 +1,63 @@ +`. `QueryPath::withXML()`, used below, always parses as + * XML, so the declaration is optional there. + * + * @author M Butcher + * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. + */ + +require_once __DIR__ . '/../../vendor/autoload.php'; + +/* + * Create a new XML document wrapped in a QueryPath. + * + * By default, the QueryPath points at the root element - here, ``. + * + * `QueryPath::withXML()` is used rather than `qp()` because it parses its input + * as XML unconditionally, so the leading XML declaration can be left off. Pass + * the same string to `qp()` and the declaration is required, because `qp()` + * decides how to treat the input by looking at it. + */ + +try { + \QueryPath\QueryPath::withXML('') + // Add a new last name inside of author. + ->append('Wiseman') + // Select all of the children of . In this case, + // that is + ->children() + // Oh, wait... we wanted last name to be inside of a + // element. Use wrap to wrap the current element in something: + ->wrap('') + // And before last name, we want to add first name. + ->before('') + // Select first name + ->prev() + // Set the text of first name + ->text('Simon') + // And then after first name, add the patronymic + ->after('J.') + // Now go back to the root element, the top of the document. + ->top() + // Add another tag -- origin. + ->append('Australia') + // turn the QueryPath contents back into a string. Since we are + // at the top of the document, the whole document will be converted + // to a string. + ->writeXML(); +} catch (\QueryPath\Exception $e) { + echo $e->getMessage(); + exit(1); +} diff --git a/examples/curl-xml-filter-and-retrieval/index.php b/examples/curl-xml-filter-and-retrieval/index.php new file mode 100644 index 00000000..c02b2165 --- /dev/null +++ b/examples/curl-xml-filter-and-retrieval/index.php @@ -0,0 +1,203 @@ +` carrying a `Name` attribute: + * + * ... + * + * That is exactly what CSS attribute selectors are for - `Item[Name="Title"]`. + * + * cURL is used here instead of handing the URL straight to `qp()` so the request + * itself can be controlled: a User-Agent, timeouts, and a retry when the API asks + * us to slow down. + * + * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. + * @see https://www.ncbi.nlm.nih.gov/books/NBK25501/ + */ + +require_once __DIR__ . '/../../vendor/autoload.php'; + +/* The API is happy with anonymous use at a few requests per second. */ +$endpoint = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/'; + +$term = 'crispr gene editing'; +$limit = 8; + +/* + * E-utilities asks callers to identify their application with a `tool` + * parameter, and to add an `email` (and an API key) if you intend to make + * requests in volume. + */ +$search_url = $endpoint . 'esearch.fcgi?' . http_build_query([ + 'db' => 'pubmed', + 'term' => $term, + 'retmax' => $limit, + 'sort' => 'date', + 'tool' => 'querypath-example', +]); + +echo '

Recent papers on ' . htmlspecialchars($term) . '

'; + +try { + /* Request one: search, and collect the IDs it comes back with. */ + $results = qp(get($search_url)); + + $ids = []; + + foreach ($results->find('IdList > Id') as $id) { + $ids[] = $id->text(); + } + + if (count($ids) === 0) { + echo '

No results found.

'; + exit; + } + + printf( + '

%s matched %s records; showing the %d most recent.

', + htmlspecialchars($term), + number_format((float) $results->top()->find('eSearchResult > Count')->text()), + count($ids) + ); + + /* + * Request two: fetch the summaries for those IDs. + * + * E-utilities takes them as a comma-separated list, so all of the records + * arrive in a single response rather than one request each. + */ + $summary_url = $endpoint . 'esummary.fcgi?' . http_build_query([ + 'db' => 'pubmed', + 'id' => implode(',', $ids), + 'tool' => 'querypath-example', + ]); + + $summaries = qp(get($summary_url)); + + /* Each record is a . */ + echo '
    '; + + foreach ($summaries->find('DocSum') as $record) { + /* + * Fields are elements distinguished by their Name attribute, so + * an attribute selector picks out the one wanted. A field that is absent + * simply matches nothing and text() returns an empty string. + */ + $title = $record->branch()->find('Item[Name="Title"]')->text(); + $journal = $record->branch()->find('Item[Name="Source"]')->text(); + $date = $record->branch()->find('Item[Name="PubDate"]')->text(); + $doi = $record->branch()->find('Item[Name="DOI"]')->text(); + + /* + * Authors are a nested list. The child combinator matters here: without + * it, the outer Item[Name="AuthorList"] would match as well. + */ + $authors = $record->branch() + ->find('Item[Name="AuthorList"] > Item[Name="Author"]') + ->textImplode(', '); + + echo '
  1. '; + printf('%s
    ', htmlspecialchars($title)); + printf('%s
    ', htmlspecialchars($authors !== '' ? $authors : 'No listed authors')); + printf('%s, %s', htmlspecialchars($journal), htmlspecialchars($date)); + + if ($doi !== '') { + printf(' · doi:%1$s', htmlspecialchars($doi)); + } + + echo '
  2. '; + } + + echo '
'; + + /* The raw XML behind the two requests, for reference. */ + echo '

The XML

'; + + echo '

Search

'; + printf('%s', htmlspecialchars($search_url)); + echo '
' . htmlspecialchars($results->top()->xml()) . '
'; + + echo '

Summaries

'; + printf('%s', htmlspecialchars($summary_url)); + echo '
' . htmlspecialchars($summaries->top()->xml()) . '
'; +} catch (Exception $e) { + echo $e->getMessage(); + exit(1); +} + +/** + * Make a GET request with cURL and return the body. + * + * A public API will occasionally ask you to slow down - E-utilities answers with + * a 429 when you exceed its request rate, and a 503 when it is briefly + * unavailable. Both are worth waiting out rather than treating as a failure, + * particularly from a shared address where somebody else may have used up the + * budget already. + * + * @param string $url + * @param int $attempts How many times to try before giving up. + * + * @return string + * @throws RuntimeException + */ +function get($url, $attempts = 4) +{ + $ch = curl_init(); + + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_HEADER => 0, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_USERAGENT => 'QueryPath/4.1 ( https://github.com/GravityPDF/querypath )', + ]); + + $status = 0; + + for ($attempt = 1; $attempt <= $attempts; $attempt++) { + if ($attempt > 1) { + /* Wait a little longer before each retry. */ + sleep(($attempt - 1) * 2); + } + + $body = curl_exec($ch); + + if ($body === false) { + $error = curl_error($ch); + curl_close($ch); + + throw new RuntimeException($error); + } + + $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); + + if ($status === 200) { + curl_close($ch); + + return $body; + } + + /* Anything else is a real failure - there is no point retrying it. */ + if ($status !== 429 && $status !== 503) { + break; + } + } + + curl_close($ch); + + throw new RuntimeException(sprintf('%s returned HTTP %d', $url, $status)); +} diff --git a/examples/database_import.php b/examples/database_import.php deleted file mode 100644 index 308fd947..00000000 --- a/examples/database_import.php +++ /dev/null @@ -1,62 +0,0 @@ - - * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. - */ - -require_once '../src/QueryPath/QueryPath.php'; -require_once '../src/QueryPath/Extension/QPDB.php'; - -// Set the default database. -QPDB::baseDB('sqlite:../test/db/qpTest.db'); - -// To begin, let's create a new database. We can do this outside -// of QueryPath: -$db = QPDB::getBaseDB(); -$db->exec('CREATE TABLE IF NOT EXISTS qpdb_article (title, author, body)'); - -// Here's our sample article: -$article = ' -
- Use QueryPath for Fun and Profit - - Matt - Butcher - - - QueryPath is a great tool.

-

Use it in many ways.

- ]]> - -
'; - -// Now let's take this article and insert it into the database: -$qp = qp($article); - -// We are going to store our insert params in here. -$params = []; - -// First, let's get the title -$params[':title'] = $qp->find('title')->text(); - -// Next, let's get the name: -$params[':name'] = $qp->top()->find('author>last')->text() . ', ' . $qp->prev('first')->text(); - -// Finally, let's get the article content: -$params[':body'] = $qp->top()->find('body')->text(); - -// Here's the query we are going to run: -$sql = 'INSERT INTO qpdb_article (title, author, body) VALUES (:title, :name, :body)'; - -// Now we can insert this: -$qp->query($sql, $params); - -// Finally, we can now read this information back out into an HTML document -qp(QueryPath::HTML_STUB, 'body')->queryInto('SELECT * FROM qpdb_article')->writeHTML(); - -// Finally, we clean up: -$qp->exec('DROP TABLE qpdb_article'); diff --git a/examples/dbpedia.php b/examples/dbpedia.php deleted file mode 100644 index b4f4f0c5..00000000 --- a/examples/dbpedia.php +++ /dev/null @@ -1,98 +0,0 @@ - - * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. - * @see http://www.w3.org/DesignIssues/LinkedData.html - * @see http://dbpedia.org - * @see sparql.php - * @see musicbrainz.php - */ - -require_once '../src/QueryPath/QueryPath.php'; - -// The URL to look up (any of these works): -$url = 'http://dbpedia.org/data/The_Beatles.rdf'; -//$url = 'http://dbpedia.org/data/Swansea.rdf'; -//$url = 'http://dbpedia.org/data/The_Lord_of_the_Rings.rdf'; -// HTTP headers: -$headers = [ - 'Accept: application/rdf,application/rdf+xml;q=0.9,*/*;q=0.8', - 'Accept-Language: en-us,en', - 'Accept-Charset: ISO-8859-1,utf-8', - 'User-Agent: QueryPath/1.2', -]; - -// The context options: -$options = [ - 'http' => [ - 'method' => 'GET', - 'protocol_version' => 1.1, - 'header' => implode("\r\n", $headers), - ], -]; - -// Create a stream context that will tell QueryPath how to -// load the file. -$cxt = stream_context_create($options); - -// Fetch the URL and select all rdf:Description elements. -// (Note that | is the CSS 3 equiv of colons for namespacing.) -// To add the context, we pass it in as an option to QueryPath. -$qp = qp($url, 'rdf|Description', ['context' => $cxt]); -//$qp = qp('The_Beatles.rdf'); - -printf("There are %d descriptions in this record.\n", $qp->size()); - -// Here, we use rdf|* to select all elements in the RDF namespace. -$qp->top()->find('rdf|*'); -printf("There are %d RDF items in this record.\n", $qp->size()); - -// Standard pseudo-classes that are not HTML specific can be used on -// namespaced elements, too. -print "About: " . $qp->top()->find('rdfs|label:first')->text() . PHP_EOL; -print "About (FOAF): " . $qp->top()->find('foaf|name:first')->text() . PHP_EOL; - -// Namespaced attributes can be retrieved using the same sort of delimiting. -print "\nComment:\n"; -print $qp->top()->find('rdfs|comment[xml|lang="en"]')->text(); -print PHP_EOL; - -$qp->top(); - -print "\nImages:\n"; -foreach ($qp->branch()->find('foaf|img') as $img) { - // Note that when we use attr() we are using the XML name, NOT - // the CSS 3 name. So it is rdf:resource, not rdf|resource. - // The same goes for the tag() function -- it will return - // the full element name (e.g. rdf:Description). - print $img->attr('rdf:resource') . PHP_EOL; -} - -print "\nImages Galleries:\n"; -foreach ($qp->branch()->find('dbpprop|hasPhotoCollection') as $img) { - print $img->attr('rdf:resource') . PHP_EOL; -} - -print "\nOther Sites:\n"; -foreach ($qp->branch()->find('foaf|page') as $img) { - print $img->attr('rdf:resource') . PHP_EOL; -} - -//$qp->writeXML(); diff --git a/examples/dirty_html.php b/examples/dirty_html.php deleted file mode 100644 index 2c2c3e68..00000000 --- a/examples/dirty_html.php +++ /dev/null @@ -1,19 +0,0 @@ -Urban Dictionary Random Word Generator'; - -$page = rand(0, 288); -$qp = htmlqp('http://www.urbandictionary.com/?page=' . $page, '#home'); - -$rand = rand(0, 7); -print $qp->find('.word')->eq($rand)->text() . '
'; -print $qp->top()->find('.definition')->eq($rand)->text(); diff --git a/examples/doc.html b/examples/doc.html deleted file mode 100644 index 4588cf18..00000000 --- a/examples/doc.html +++ /dev/null @@ -1,109 +0,0 @@ - - - - -Documentation - - - - -
-
-
-
-
-
-
-
-
-
- - - \ No newline at end of file diff --git a/examples/doc.php b/examples/doc.php deleted file mode 100644 index f80d7eb7..00000000 --- a/examples/doc.php +++ /dev/null @@ -1,108 +0,0 @@ -$v
"; -} - -// The document skeleton -$qpdoc = htmlqp('doc.html', 'body'); - -$key = $_GET['key']; - -// The jQuery categories that are used in QueryPath -$qparray = [ - 'Tree Traversal', - 'Child Filter', - 'Attribute', - 'Content Filter', - 'Basic Filter', - 'Hierarchy', - 'Basic', - 'Filtering', - 'Miscellaneous Traversing', - 'DOM Insertion, Outside', - 'DOM Insertion, Inside', - 'Attributes', - 'Style Properties', -]; - -$jqnames = []; -$qpnames = []; - -// Search through the xml file to find any entries of jQuery entities -foreach (qp('querypath.xml', 'entry') as $entry) { - $qpnames[$entry->attr('name')] = - [ - 'desc' => $entry->find('desc')->innerXML(), - 'jquery' => $entry->parent()->find('jquery')->innerXML(), - 'querypath' => $entry->parent()->find('querypath')->innerXML(), - ]; -} - -// Search through the xml file to find all entries of jQuery entities -foreach (htmlqp('http://api.jquery.com/api/', 'entry') as $entry) { - $category = false; - $category = array_search($entry->find('category:first')->attr('name'), $qparray); - while ($entry->next('category')->html() != null) { - $category = (array_search($entry->attr('name'), $qparray)) ? true : $category; - if ($category) { - break; - } - } - if ($category) { - $jqnames[$entry->parent()->attr('name')] = - [ - 'longdesc' => $entry->find('longdesc')->innerXML(), - 'name' => $entry->parent()->find('category')->attr('name'), - ]; - } -} - -// Map the keys & sort them -$jqkeys = array_keys($jqnames); -$jqkeys = array_map("addClasses", $jqkeys); -sort($jqkeys); - -// Add the keys to the nav bar -$qpdoc->find('#leftbody'); -foreach ($jqkeys as $k => $v) { - $qpdoc->append($v); -} - -// Add the description to the main window if the key exists -if (array_key_exists($key, $jqnames)) { - if (array_key_exists($key, $qpnames)) { - $qpdoc->top()->find('#rightfunction')->text('Function: ' . ucfirst($key)); - $qpdoc->top()->find('#rightdesc')->text($qpnames[$key]['desc']); - $qpdoc->top()->find('#righttitle')->text('How it\'s done in jQuery'); - $qpdoc->top()->find('#righttext')->text($qpnames[$key]['jquery']); - $qpdoc->top()->find('#righttitle2')->text('How it\'s done in QueryPath'); - $qpdoc->top()->find('#righttext2')->text($qpnames[$key]['querypath']); - } else { - $qpdoc->top()->find('#rightfunction')->text('Function: ' . ucfirst($key)); - $qpdoc->top()->find('#rightdesc')->remove(); - $qpdoc->top()->find('#righttitle')->text('jQuery Documentation'); - $qpdoc->top()->find('#righttext')->append($jqnames[$key]['longdesc']); - } -} - -// Write the document -$qpdoc->writeHTML(); diff --git a/examples/docx.php b/examples/docx.php deleted file mode 100644 index e917c099..00000000 --- a/examples/docx.php +++ /dev/null @@ -1,116 +0,0 @@ -branch(); - print format($qr->find('w|r:first'), 'w|r:first') . ' '; - $qp->find('w|r:first'); - while ($qp->next('w|r')->html() != null) { - $qr = $qp->branch(); - print format($qr->find('w|r'), 'w|r') . ' '; - // print $qp->text(); - } - print '
'; -} - -/** - * - * @param QueryPath $qp - * @param String $findSelector - * - * @return String - */ -function format($qp, $findSelector = null) -{ - - // Create a new branch for printing later. - $qr = $qp->branch(); - - $text = ""; - - $text = $qr->find($findSelector)->find('w|t')->text(); - - $text = (checkUnderline($qp->branch())) ? '' . $text . '' : $text; - $text = (checkBold($qp->branch())) ? '' . $text . '' : $text; - - return $text; -} - -/** - * - * @param QueryPath $qp - * - * @return String - */ -function checkBold($qp) -{ - $qp->children("w|rPr"); - - return ($qp->children('w|b')->html()) ? true : false; -} - -/** - * - * @param QueryPath $qp - * - * @return String - */ -function checkUnderline($qp) -{ - $qp->children("w|rPr"); - - return ($qp->children('w|u')->html()) ? true : false; -} - - -function docx2text($filename) -{ - return readZippedXML($filename, "word/document.xml"); -} - -function readZippedXML($archiveFile, $dataFile) -{ - if (! class_exists('ZipArchive', false)) { - return "ZipArchive Class Doesn't Exist."; - } - // Create new ZIP archive - $zip = new ZipArchive(); - // Open received archive file - if (true === $zip->open($archiveFile)) { - // If done, search for the data file in the archive - if (($index = $zip->locateName($dataFile)) !== false) { - // If found, read it to the string - $data = $zip->getFromIndex($index); - // Close archive file - $zip->close(); - // Load XML from a string - // Skip errors and warnings - return $data; - // $xml = DOMDocument::loadXML($data, LIBXML_NOENT | LIBXML_XINCLUDE | LIBXML_NOERROR | LIBXML_NOWARNING); - // // Return data without XML formatting tags - // return strip_tags($xml->saveXML()); - } - $zip->close(); - } - - // In case of failure return empty string - return $zip->getStatusString(); -} diff --git a/examples/docx_document.xml b/examples/docx_document.xml deleted file mode 100644 index fcf8614f..00000000 --- a/examples/docx_document.xml +++ /dev/null @@ -1,3914 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - I. Para - - - - - - - - - : - - - - - - - - - - - - - - - - - - - - - - - 1. In order to + infinitive (Hago ejercicos - - - - - - - - - - para - - - - - - - - rebajar) (Tomo la medicina - - - - - - - - - - para - - - - - - - - sentirme mejor) - - - - - - - - - - - - - - - - - - - - - - - - - 2. For = destined for, to be given to (Todo mi amor es - - - - - - - - - - - para - - - - - - - - - ti) - - - - - - - - - - - - - - - - - - - - - - - 3. For = by deadline, specified future time (La tarea es - - - - - - - - - - para - - - - - - - - el lunes) - - - - - - - - - - - - - - - - - - - - - - - (Es - - - - - - - - - - para - - - - - - - - las cinco de la tarde) - - - - - - - - - - - - - - - - - - - - - - - - - 4. For = toward in the direction of (destination) (Sarah salió - - - - - - - - - - - para - - - - - - - - - España) - - - - - - - - - - - - - - - - - - - - - - - (Manolo Salió - - - - - - - - - - para - - - - - - - - el campo) - - - - - - - - - - - - - - - - - - - - - - - - - 5. For = to be used for (purpose of an object) (La taza es - - - - - - - - - - - para - - - - - - - - - el café) - - - - - - - - - - - - - - - - - - - - - - - (El dinero es - - - - - - - - - - para - - - - - - - - la matricula) - - - - - - - - - - - - - - - - - - - - - - - 6. For = as compared with others, in relation to others ( - - - - - - - - - - - Para - - - - - - - - mí, el español es fácil) ( - - - - - - - - - - Para - - - - - - - - ser extranjero, habla muy bien el inglés) - - - - - - - - - - - - - - - - - - - - - - - - - 7. For = in the employ - - - - - - - - ment - - - - - - - - of (Trabajan - - - - - - - - - - para - - - - - - - - el gobierno) (Nosotros trabajamos - - - - - - - - - - para - - - - - - - - la universidad) - - - - - - - - - - - - - - - - - - - - - - - - - - - II - - - - - - - - - - . Por: - - - - - - - - - - - - - - - - - - - - - - - 1. By means of (voy a España - - - - - - - - - - por - - - - - - - - avión) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 2. Through, along ( Camino por el parque) (Paseo - - - - - - - - - - por - - - - - - - - la ciudad) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 3. Duration of time, time of day, (Trabajo - - - - - - - - - - por - - - - - - - - la noche, trabajo - - - - - - - - - - por - - - - - - - - cinco horas) - - - - - - - - - - - - - - - - - - - - - - - (Voy a Perú - - - - - - - - - - por - - - - - - - - un año) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 4. Because of, due to (Estoy nervioso - - - - - - - - - - por - - - - - - - - la entrevista) (Megan tiene tos - - - - - - - - - - por - - - - - - - - el resfriado) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5. For = in Exchange for (Te cambio mi BMW - - - - - - - - - - por - - - - - - - - tu Mercedes) - - - - - - - - - - - - - - - - - - - - - - - (Compré un CD - - - - - - - - - - por - - - - - - - - 10 dólares) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 6. For = for the sake of, on behalf of (Lo hago - - - - - - - - - - - por - - - - - - - - - ti) (Trabajo - - - - - - - - - - - por - - - - - - - - - los niños) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 7. To do something for someone else (a favor) (Esta semana, Juan va a trabajar - - - - - - - - - - por - - - - - - - - María) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Por is also used with some “fixed expressions” (Know them) - - - - - - - - - - - - - - - - - - - - - - - - - - - - Por Dios - - - - - - - - - (for heaven’s sake) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Por ejemplo - - - - - - - - (for example) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Por ahí, por allí - - - - - - - - - - (around there) - - - - - - - - - - - - - - - - - - - - - - - - - - - Por eso - - - - - - - - - (that’s why) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Por cierto - - - - - - - - - (by the way) - - - - - - - - - - - - - - - - - - - - - - - - - Por favor - - - - - - - - (please) - - - - - - - - - - - - - - - - - - - - - - - - - Por fin - - - - - - - - (finally) - - - - - - - - - - - - - - - - - - - - - - - - - Por lo general - - - - - - - - (in general, generally) - - - - - - - - - - - - - - - - - - - - - - - - - Por lo menos - - - - - - - - (at least) - - - - - - - - - - - - - - - - - - - - - - - - - - - Por primera/última vez - - - - - - - - - (for the first/last time) - - - - - - - - - - - - - - - - - - - - - - - - - Por si acaso - - - - - - - - (just in case) - - - - - - - - - - - - - - - - - - - - - - - - - Por supuesto - - - - - - - - (of course) - - - - - - - - - - - - - - - - - - - - - - - - - Por todas partes - - - - - - - - (everywhere) - - - - - - - - - - - - - - - - - - - - - - - - - Por ahora - - - - - - - - (for now) - - - - - - - - - - - - - - - - - - - - - - - - - Por lo visto - - - - - - - - (apparently) - - - - - - - - - - - - - - - - - - - - - - - - - Por último - - - - - - - - (finally) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A. - - - - - - - - - - - - - - - - - - - - Por y Para 1- - - - - - - - - - Un viaje a Costa Rica-Write por or para in the lines below, depending on the context. - - - - - - - - - - - - - - - - - - - - - - - - - Ellos hicieron un viaje a Costa Rica ______________ - (1)cinco semanas. Viajaron ______________(2) avión y cuando - llegaron al país, tomaron unos taxis______________(3) ir a su - hotel.____________(4) ser viajeros en un país extranjero, - tenían mucho miedo al principio del viaje. Cuando llegaron al - hotel, entraron _____________(5) las puertas principales y - subieron ______________(6) el elevador para llegar a sus - propias habitaciones. Estaban emocionados de ir a este nuevo - país____________(7) conocer sus lugares turisticos. - ____________(8) disfrutar del lugar, ellos salían a los discos - ___________(9) las noches. Después de entrar en las - discos___________(10) bailar, podían comprar bebidas caras. - Casi siempre cuesta menos de dos dólares_____________(11) una - cerveza. Pero hay muchas otras cosas que hicieron durante su - visita. Ellos no fueron allá solamente ___________(12) - festejar. Hay muchas cosas diferentes en Costa Rica que no hay - en los Estados Unidos.__________(13) ejemplo, el paisaje es - algo que no se puede encontrar en todas partes del mundo. La - mayoría de los estudiantes gozaron su viaje y les - - - - - - - - - - - - - - - - - - gustaría ir__________(14) Latinoamérica otra vez. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - B. Por y Para 2- - - - - - - - - Complete los siguientes diálogos y oraciones utilizando por - o para: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1. Carla, Kyra y Melissa salieron__________Pepe’s ayer. Van - a Pepe’s _________coche y luego de ir a Pepe’s irán a pa - - - - - - - - s - - - - - - - - ear _________toda la ciudad. Va a ser una buena experiencia - ________las tres. - - - - - - - - - - - - - - - - - - - - - - - 2. Mi hermano Miguel quiere estudiar __________ser doctor. - ________eso, trabaja _________mi madre que es médica. Trabaja - _________las mañanas y __________las tardes. Trabaja - _________muchas horas - - - - - - - - - - - - - - - - _________obtener más experiencia. - - - - - - - - - - - - - - - - - - - - - - - 3. ________lo general, Joel es una persona muy amable. - ________ejemplo, la semana pasado nos trajo donas y galletas. - - - - - - - - - - - - - - - - - - - - - - - - 4. El cuadro de Guernica fue pintado ________Pablo Picasso. - Lo pintó _______representar las atrocidades de la guerra. - - - - - - - - - - - - - - - - - - - - - - - - 5. Asia, Angie y Erica iban a dar un paseo __________el - parque pero lo cancelaron _________el mal tiempo (bad weather) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - C. - - - - - - - - - - - - - Pretérito e Imperfecto - - - - - - - - - - - - - - - - - - - - - - - Gloria Fernández, the graduate student who spends many hours daily in front of the computer, wants to tell her friend Kathy about her visit to the doctor, so she sends her an e-mail. The main frame, however, is acting up, and some of the verbs have been deleted. Help Kathy complete the message by providing the missing verbs in the appropriate tense—either the preterite or the imperfect. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - correo_para_kathy@abc.edu - - - - - - - - - - - - - - - - - - - - - Tema: Mi visita al doctor - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Querida Kathy: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Ayer fui al doctor porque no (sentirse) __________________ - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - - bien: me (doler) __________________ - - - - - - - - - - - - 2 - - - - - - - - - - - - - - - - - - - - - - la cabeza y (tener) __________________ - - - - - - - - - - - - 3 - - - - - - - - - - - - - - - - - - - - - - los ojos irritados. Primero, el doctor me (tomar) - _______________ - - - - - - - - - - - - 4 - - - - - - - - - - - - - - - - - - - - - - la temperatura, pero no (tener) ____________ - - - - - - - - - - - - 5 - - - - - - - - - - - - - - - - - - - - - - fiebre. Después me (examinar) _______________ - - - - - - - - - - - - 6 - - - - - - - - - - - - - - - - - - - - - - los ojos y me (preguntar) __________________ - - - - - - - - - - - - 7 - - - - - - - - - - - - - - - - - - - - - - si (trabajar) __________________ - - - - - - - - - - - - 8 - - - - - - - - - - - - - - - - - - - - - - mucho con la computadora; yo le (decir) __________________ - - - - - - - - - - - - - 9 - - - - - - - - - - - - - - - - - - - - - - que sí. Entonces él me (recomendar) __________________ - - - - - - - - - - - - - 10 - - - - - - - - - - - - - - - - - - - - - - que tomara - - - - - - - - - - - - - - - - - - - - - - - descansos y relajara - - - - - - - - - - - - - - - - - - - - - - - los ojos, porque las computadoras son malas para la vista. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - D. - - - - - - - - - - - - - Unplanned/Unexpected Events - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Pamela is at work and her friend Susana arrives late. She has had a horrible morning. Pamela discusses Susana’s horrible morning with her coworkers during her lunch break. - - - - - - - - - - - Following the model, use the cues and the verbs in parentheses to write complete sentences with - - - - - - - - - - - - - se - - - - - - - - - - - describing Susana’s actions. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MODELO - - - - - - - - - - - : - - - - - - - - - - - - When she tells them that Susana forgot her keys in her - house, she says - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (olvidar) - - - - - - - - - - - - A Susana se le olvidaron las llaves en su casa. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1. - - - - - - - - - - - - When she tells them that Susana lost her cellular phone, - she says: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (perder) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 2. - - - - - - - - - - - - When she tells them that Susana ‘s car ran out of gas she - says: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (acabar) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 3. - - - - - - - - - - - - When she tells them that Susana’s glasses broke, she says: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (romper)_______________________________________________________________________ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 4. - - - - - - - - - - - - When she tells them that Susana left her documents at home, - she says: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (quedar) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5. - - - - - - - - - - - - When she tells them that Susana dropped her coffee, she - says: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - (caer) - _________________________________________________________________________ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - E - - - - - - - - - - - - - . Los Adverbios- - - - - - - - - - - - Change the adjectives in parentheses to adverbs. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - La doctora le dice a Pablo que todo va a ir bien: - - - - - - - - - - - - - - - - - - - - - Señor Ortiz, su salud está - - - - - - - - - - - - (perfecto) ________________ - - - - - - - - - - - - 1 - - - - - - - - - - - bien. Su peso está bajando (rápida) ________________ - - - - - - - - - - - - 2 - - - - - - - - - - - de 220 libras a 180 y parece que hace ejercicios - (frecuente) ________________ - - - - - - - - - - - - 3. - - - - - - - - - - - Necesita una dieta (total) ________________ - - - - - - - - - - - - 4 - - - - - - - - - - - baja en grasas para mantener su peso. Además, quiero que - escuche mis consejos (cuidadoso) ________________ - - - - - - - - - - - - 5 - - - - - - - - - - - y espero que vaya a casa más (tranquilo) - - - - - - - - - - - _______________________________. - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/fetch_rss.php b/examples/fetch_rss.php deleted file mode 100644 index 44767bc9..00000000 --- a/examples/fetch_rss.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. - */ -require_once '../src/QueryPath/QueryPath.php'; - -// The URL of the remote RSS feed. -$remote = 'http://querypath.org/aggregator/rss/2/rss.xml'; - -// We will write the results into this document. -$out = qp(QueryPath::HTML_STUB, 'title') - ->text('RSS Titles') - ->top() - ->find('body') - ->append('
    ') - ->children('ul'); - -// Load the remote document and loop through all of the items. -foreach (qp($remote, 'channel>item') as $item) { - // Get title and link. - $title = $item->find('title')->text(); - $link = $item->next('link')->text(); - - // Do a little string building. - $bullet = '
  • ' . $title . '
  • '; - - // Add it to the output document. - $out->append($bullet); -} - -// Write the results. -$out->writeHTML(); diff --git a/examples/filtering-by-text-content/index.php b/examples/filtering-by-text-content/index.php new file mode 100644 index 00000000..29643a8f --- /dev/null +++ b/examples/filtering-by-text-content/index.php @@ -0,0 +1,51 @@ + + * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. + */ + +require_once __DIR__ . '/../../vendor/autoload.php'; + +try { + $qp = html5qp('https://www.php.net'); + + echo '

    Filtering Content

    '; + echo '

    PHP Releases

    '; + + /* Get any posts containing the word 'Release' */ + echo $qp->find('h2.newstitle a:contains(Release)') + ->textImplode('
    ' . PHP_EOL); + + echo '

    PHP news in the past 30 days...

    '; + + echo $qp->find('header.title') + ->filterCallback(function ($index, $item) { + /* + * Returns TRUE to keep current $item in matches, or FALSE to remove + * + * $item is a DOMNode (actually, a DOMElement). So if we wanted to do QueryPath + * manipulations on it, you can pass it to html5qp() + */ + + /* Get the current post datetime */ + $datetime = new DateTimeImmutable(html5qp($item, 'time')->attr('datetime')); + + /* Keep any posts less than 30 days old */ + return $datetime > (new DateTimeImmutable('-30 days')); + }) + ->find('a') + ->textImplode('
    ' . PHP_EOL); +} catch (\QueryPath\Exception $e) { + echo $e->getMessage(); + exit(1); +} diff --git a/examples/generating-rss-feed/index.php b/examples/generating-rss-feed/index.php new file mode 100644 index 00000000..90dc3003 --- /dev/null +++ b/examples/generating-rss-feed/index.php @@ -0,0 +1,115 @@ + + * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. + * @see https://www.rssboard.org/rss-specification + */ + +require_once __DIR__ . '/../../vendor/autoload.php'; + +/* The stub for the feed itself. */ +$rss_stub = ' + + + + + + en + QueryPath + +'; + +/* The stub for a single entry in the feed. */ +$rss_item_stub = ' + + + + + + + + +'; + +/* + * The entries to render. A nested array keeps the example self-contained; in a + * real application this would be a database result, an API response, and so on. + */ +$items = [ + [ + 'title' => 'Item 1', + 'link' => 'https://example.com/item1', + 'description' => 'This has embedded HTML', + 'comments' => 'https://example.com/item1/comments', + 'category' => 'Some Term', + 'pubDate' => date('r'), + 'guid' => '123456-789', + ], + [ + 'title' => 'Item 2', + 'link' => 'https://example.com/item2', + 'description' => 'This has embedded HTML', + 'comments' => 'https://example.com/item2/comments', + 'category' => 'Some Other Term', + 'pubDate' => date('r'), + 'guid' => '123456-790', + ], +]; + +try { + /* + * Load the feed stub with selected, fill in the channel metadata, + * then step back up to <channel> so items can be appended to it. + */ + $qp = qp($rss_stub, 'channel > title') + ->text('A QueryPath RSS Feed') + // next() moves to the following sibling, so the fields are filled in + // document order without re-querying each time. + ->next('link')->text('https://example.com') + ->next('description')->text('QueryPath: Find your way.') + ->parent(); + + foreach ($items as $item) { + /* + * Each entry gets its own QueryPath built from the item stub. The keys of + * $item are in the same order as the elements in the stub, so the fields + * can be walked with next() as above. + * + * text() encodes its argument, so the HTML in 'description' is written + * out as <strong>... - exactly what an RSS reader expects. + */ + $qpi = qp($rss_item_stub, 'title')->text($item['title']); + + foreach (['link', 'description', 'comments', 'category', 'pubDate', 'guid'] as $field) { + $qpi = $qpi->next()->text($item[$field]); + } + + /* top() returns to <item> - the whole fragment - before appending it. */ + $qp->append($qpi->top()); + } + + /* + * When serving this over HTTP the content type needs to be set before any + * output is written: + * + * header('Content-Type: application/rss+xml'); + */ + $qp->top()->writeXML(); +} catch (\QueryPath\Exception $e) { + // Handle QueryPath exceptions + echo $e->getMessage(); + exit(1); +} diff --git a/examples/hello-world/index.php b/examples/hello-world/index.php new file mode 100644 index 00000000..68692266 --- /dev/null +++ b/examples/hello-world/index.php @@ -0,0 +1,74 @@ +<?php +/** + * Hello World + * + * The smallest useful QueryPath program, and a good place to start. + * + * Three functions do most of the work in QueryPath, and all three return a + * `\QueryPath\DOMQuery`: + * + * qp() parses XML and XHTML with libxml + * htmlqp() parses legacy (pre-HTML5) HTML with libxml + * html5qp() parses HTML5 with masterminds/html5 - the recommended choice for HTML + * + * From there the API mirrors jQuery: `find()` selects, methods like `text()` and + * `attr()` read or change the selection, and every call returns a QueryPath so + * calls can be chained. + * + * @author M Butcher <matt@aleph-null.tv> + * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. + * @see https://github.com/GravityPDF/querypath + */ + +require_once __DIR__ . '/../../vendor/autoload.php'; + +try { + /* + * QueryPath ships with an empty HTML5 document to build on: + * \QueryPath\QueryPath::HTML5_STUB. Parse it, select the <body>, set some + * text, and print the result. + * + * writeHTML5() prints the whole document. Use html5() instead if you want + * the markup returned as a string. + */ + html5qp(\QueryPath\QueryPath::HTML5_STUB) + ->find('body') + ->text('Hello World') + ->writeHTML5(); + + /* + * Reading is just as short. Pass a markup string (or a file path, or a URL) + * to the factory, select what you want, and pull the value out. + */ + $html = '<ul id="cast"> + <li class="name">John</li> + <li class="name">Paul</li> + <li class="name">George</li> + <li class="name">Ringo</li> + </ul>'; + + echo PHP_EOL; + + /* text() on a single match returns that element's text. */ + echo 'First name: ' . html5qp($html)->find('#cast .name')->eq(0)->text() . PHP_EOL; + + /* count() reports how many elements matched the selector. */ + echo 'How many: ' . html5qp($html)->find('#cast .name')->count() . PHP_EOL; + + /* textImplode() joins the text of every match with a separator. */ + echo 'Everyone: ' . html5qp($html)->find('#cast .name')->textImplode(', ') . PHP_EOL; + + /* + * Changing a document is the same idea. Here every name gets a class, and + * the modified list is returned as a string rather than printed. + */ + echo PHP_EOL . html5qp($html) + ->find('#cast .name') + ->addClass('beatle') + ->parents('#cast') + ->html() . PHP_EOL; +} catch (\QueryPath\Exception $e) { + // Handle QueryPath exceptions + echo $e->getMessage(); + exit(1); +} diff --git a/examples/html.php b/examples/html.php deleted file mode 100644 index 199ef26d..00000000 --- a/examples/html.php +++ /dev/null @@ -1,56 +0,0 @@ -<?php -/** @file - * Using QueryPath. - * - * This file contains an example of how QueryPath can be used - * to generate web pages. Part of the design of this example is to exhibit many - * different QueryPath functions in one long chain. All of the methods shown - * here are fully documented in {@link QueryPath}. - * - * The method used in this example is a typical example of how QueryPath can - * gradually build up content. Other methods include using {@link QPTPL} for - * templates, injecting database information with {@link QPDB}, and merging - * data from one QueryPath to another. - * - * @author M Butcher <matt@aleph-null.tv> - * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. - */ - -require_once '../src/qp.php'; - -// Begin with an HTML stub document (XHTML, actually), and navigate to the title. -qp(QueryPath::HTML_STUB, 'title') - // Add some text to the title - ->text('Example of QueryPath.') - // Now look for the <body> element - ->top('body') - // Inside the body, add a title and paragraph. - ->append('<h1>This is a test page</h1><p>Test text</p>') - // Now we select the paragraph we just created inside the body - ->children('p') - // Add a 'class="some-class"' attribute to the paragraph - ->attr('class', 'some-class') - // And add a style attribute, too, setting the background color. - ->css('background-color', '#eee') - // Now go back to the paragraph again - ->parent() - // Before the paragraph and the title, add an empty table. - ->prepend('<table id="my-table"></table>') - // Now let's go to the table... - ->top('#my-table') - // Add a couple of empty rows - ->append('<tr></tr><tr></tr>') - // select the rows (both at once) - ->children() - // Add a CSS class to both rows - ->addClass('table-row') - // Now just get the first row (at position 0) - ->eq(0) - // Add a table header in the first row - ->append('<th>This is the header</th>') - // Now go to the next row - ->next() - // Add some data to this row - ->append('<td>This is the data</td>') - // Write it all out as HTML - ->writeHTML(); diff --git a/examples/http-stream-xml-namespaces-and-linked-data/index.php b/examples/http-stream-xml-namespaces-and-linked-data/index.php new file mode 100644 index 00000000..3cdc62c9 --- /dev/null +++ b/examples/http-stream-xml-namespaces-and-linked-data/index.php @@ -0,0 +1,83 @@ +<?php +/** + * Example of grabbing and parsing Linked Data from DBPedia. + * + * This example illustrates how QueryPath can be used to do the following: + * + * - Make a robust HTTP connection to a remote server to fetch data. + * - Using context to control the underlying stream. + * - Working with Linked Data. + * - Work with XML Namespaces in documents. + * * Using namespaces to access elements in selectors + * * Using namespaces to access attributes in selectors + * * Using namespaces to access attributes in XML methods. + * + * The code here connects to the DBPedia server and looks up the Linked + * Data stored there for a particular Wikipedia entry (any Wikipedia + * wiki name should work here). + * + * + * @author M Butcher <matt@aleph-null.tv> + * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. + * @see http://www.w3.org/DesignIssues/LinkedData.html + * @see http://dbpedia.org + * @see ../sparql-endpoint-query/index.php + * @see ../curl-xml-filter-and-retrieval/index.php + */ + +require_once __DIR__ . '/../../vendor/autoload.php'; + +// The URL to look up: +$url = 'https://dbpedia.org/resource/Ben_Sealey'; + +// HTTP headers: +$headers = [ + 'Accept: application/rdf,application/rdf+xml;q=0.9,*/*;q=0.8', + 'Accept-Language: en-us,en', + 'Accept-Charset: ISO-8859-1,utf-8', + 'User-Agent: QueryPath/1.2', +]; + +// The context options: +$options = [ + 'http' => [ + 'method' => 'GET', + 'protocol_version' => 1.1, + 'header' => implode("\r\n", $headers), + ], +]; + +// Create a stream context that will tell QueryPath how to load the file. +$context = stream_context_create($options); + +try { + // Fetch the URL and select all rdf:Description elements. + // (Note that | is the CSS 3 equiv of colons for namespacing.) + // To add the context, we pass it in as an option to QueryPath. + $qp = qp($url, 'rdf|Description', ['context' => $context]); + + printf('There are %d descriptions in this record.<br>' . PHP_EOL, $qp->count()); + + // Here, we use foaf|* to select all elements in the FOAF namespace. + printf('There are %d DBO items in this record.<br><br>' . PHP_EOL, $qp->top()->find('dbo|*')->count()); + + // Standard pseudo-classes that are not HTML specific can be used on namespaced elements, too. + echo 'About (RDFS): ' . $qp->top()->find('rdfs|label:first-of-type')->text() . '<br>' . PHP_EOL; + echo 'About (FOAF): ' . $qp->top()->find('foaf|name:first-of-type')->text() . '<br>' . PHP_EOL; + + // Namespaced attributes can be retrieved using the same sort of delimiting. + echo PHP_EOL . '<br>Comment:<br>' . PHP_EOL; + echo $qp->top()->find('rdfs|comment[xml|lang="en"]')->text(); + echo '<br>' . PHP_EOL; + + $qp->top(); + + echo PHP_EOL . '<br>Other Sites:<br>' . PHP_EOL; + foreach ($qp as $item) { + echo $item->attr('rdf:about') . '<br>' . PHP_EOL; + } +} catch (\QueryPath\Exception $e) { + // Handle QueryPath exceptions + echo $e->getMessage(); + exit(1); +} diff --git a/examples/iterating-over-matches/index.php b/examples/iterating-over-matches/index.php new file mode 100644 index 00000000..6618cb1e --- /dev/null +++ b/examples/iterating-over-matches/index.php @@ -0,0 +1,119 @@ +<?php +/** + * Iterating over a set of matches + * + * A QueryPath wraps zero or more nodes, and most of its methods operate on the + * whole set at once. Sometimes you need to work through the matches one at a + * time instead. This example shows five ways of doing that. + * + * Keep in mind that PHP hands objects around by handle, so a change made to an + * element inside a loop is reflected in the QueryPath it came from. There is no + * need to put anything back afterwards. + * + * @author M Butcher <matt@aleph-null.tv> + * @license LGPL (The GNU Lesser GPL) or an MIT-like license. + */ + +require_once __DIR__ . '/../../vendor/autoload.php'; + +$xml = '<?xml version="1.0"?> +<data> + <li>One</li> + <li>Two</li> + <li>Three</li> + <li>Four</li> + <li>Five</li> +</data>'; + +try { + $qp = qp($xml, 'li'); + + /* + * 1. foreach over the QueryPath itself. + * + * QueryPath is iterable, and yields a new DOMQuery for each match, so the + * full API is available inside the loop. This is usually what you want. + */ + echo '1. foreach over the QueryPath' . PHP_EOL; + + foreach ($qp as $item) { + echo ' ' . $item->tag() . ': ' . $item->text() . PHP_EOL; + } + + /* + * 2. foreach over the raw DOM nodes. + * + * get() with no arguments returns the underlying DOMElement objects. Use + * this when you want to drop down to the DOM API and skip the QueryPath + * wrapper entirely. + */ + echo PHP_EOL . '2. foreach over the DOM nodes' . PHP_EOL; + + foreach ($qp->get() as $element) { + echo ' ' . $element->tagName . ': ' . $element->textContent . PHP_EOL; + } + + /* + * 3. each() with a closure. + * + * The callback receives the index and the DOMElement, and returning false + * stops the loop early - handy when you are searching rather than visiting. + * each() returns the same QueryPath, so it can be used mid-chain. + */ + echo PHP_EOL . '3. each() with a closure' . PHP_EOL; + + $qp->each(function ($index, $element) { + echo ' ' . $index . ' => ' . $element->textContent . PHP_EOL; + }); + + /* + * 4. each() with a named function. + * + * Anything PHP accepts as a callable works, so a function name or an + * [$object, 'method'] pair can be reused across several calls. + */ + echo PHP_EOL . '4. each() with a named function' . PHP_EOL; + + $qp->each('printItem'); + + /* + * 5. Index-based access. + * + * count() reports how many matches there are, and get($i) returns the DOM + * node at that position. Use eq($i) instead if you want a QueryPath back. + */ + echo PHP_EOL . '5. By index' . PHP_EOL; + + for ($i = 0; $i < $qp->count(); $i++) { + echo ' ' . $qp->get($i)->textContent . PHP_EOL; + } + + /* + * Changes made during a loop stick, because the loop is working on the + * document itself rather than on a copy of it. + */ + echo PHP_EOL . 'Matches can be modified in place:' . PHP_EOL; + + $qp->each(function ($index, $element) { + $element->setAttribute('data-position', $index + 1); + }); + + echo $qp->top()->xml(); +} catch (\QueryPath\Exception $e) { + // Handle QueryPath exceptions + echo $e->getMessage(); + exit(1); +} + +/** + * Print a single match. Used by the each() example above. + * + * @param int $index + * @param \DOMElement $element + * + * @return void + */ +function printItem($index, $element) +{ + echo ' ' . $index . ' => ' . $element->textContent . PHP_EOL; +} diff --git a/examples/matching_text_content.php b/examples/matching_text_content.php deleted file mode 100644 index 69891260..00000000 --- a/examples/matching_text_content.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php -/** - * Matching Text Content. - * - * This example shows one way of matching text content. - * The `:contains()` pseudo-class requires that the ENTIRE CONTENTS - * of an element match exactly. But sometimes what we want is a way - * to match just part of the contents of an element. This example - * illustrates how to accomplish this with a filter callback. - * - * As of QueryPath 2.1beta2, `:contains()` performs a substring match instead of - * and exact match, so the method outline below is roughly the same as merely - * using `:contains(Release)`. - * - * - * @author M Butcher <matt@aleph-null.tv> - * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. - */ - -/** Include QueryPath. */ -require_once '../src/QueryPath/QueryPath.php'; - -/** - * Check if the string 'Release' is in the text content of any matched nodes. - * - * Returns TRUE if the text is found, FALSE otherwise. Anytime a filter callback - * returns FALSE, QueryPath will remove it from the matches. - * - * Note that $item is a DOMNode (actually, a DOMElement). So if we wanted to do QueryPath - * manipulations on it, we could wrap it in a `qp()`. - */ -function exampleCallback($index, $item) -{ - $text = qp($item)->text(); - - return strpos($text, 'Release') !== false; -} - -/* - * This is the QueryPath call. - * - * First we fetch the remote page, parse it, and grab just the `a` tags inside of the summary. - * Then we filter the results through our callback. - * Finally, we fetch all of the matching text and print it. - * - * NOTE: If you are using PHP 5.3, you can define the callback inline instead of separating it - * into a stand-alone function. - */ -print htmlqp('http://php.net/', 'h1.summary a') - ->filterCallback('exampleCallback') - ->textImplode(PHP_EOL); diff --git a/examples/musicbrainz.php b/examples/musicbrainz.php deleted file mode 100644 index 480bf998..00000000 --- a/examples/musicbrainz.php +++ /dev/null @@ -1,40 +0,0 @@ -<?php -/** - * Do an XML lookup from MusicBrainz. - * - * This example shows how to make a simple REST-style request against a remote - * server. (For a more advanced example of HTML requests, see {@link sparql.php}) - * - * This does two HTTP requests -- one to get information about a band, and another - * to get a list of albums put out by that band. - * - * TODO: Fix the output. - * - * - * @author M Butcher <matt@aleph-null.tv> - * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. - * @see http://musicbrainz.org - */ -require_once '../src/QueryPath/QueryPath.php'; - -$artist_url = 'http://musicbrainz.org/ws/1/artist/?type=xml&name=u2'; -$album_url = 'http://musicbrainz.org/ws/1/release/?type=xml&artistid='; -try { - $artist = qp($artist_url, 'artist:first'); - if ($artist->size() > 0) { - $id = $artist->attr('id'); - print '<p>The best match we found was for ' . $artist->children('name')->text() . PHP_EOL; - print '</p><p>Artist ID: ' . $id . PHP_EOL; - print '</p><p>Albums for this artist' . PHP_EOL; - print '</p><p><a href="' . $album_url . urlencode($id) . '">' . $album_url . '</a></p>'; - $albums = qp($album_url . urlencode($id))->writeXML(); - - foreach ($albums as $album) { - print $album->find('title')->text() . PHP_EOL; - // Fixme: Label is broken. See Drupal QueryPath module. - print '(' . $album->next('label')->text() . ')' . PHP_EOL; - } - } -} catch (Exception $e) { - print $e->getMessage(); -} diff --git a/examples/odt.php b/examples/odt.php deleted file mode 100644 index 1028e494..00000000 --- a/examples/odt.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php -/** - * Using QueryPath to parse an Open Document Type (ODT) text file. - * - * The ODT format is a standard way of representing word processing documents. - * It is based on a combination of technologies, one of which is XML. Document - * text is stored in a structural (semantic) XML document, while other information, - * such as style sheets, are stored in auxilliary files. - * - * This example shows how ODT files can be opened and manipulated using QueryPath. - * - * The file used here was generated by OpenOffice 3.x. Other word processors are - * also capable of generating ODT files. - * - * - * @author M Butcher <matt@aleph-null.tv> - * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. - */ - -/** Include main QP library. */ -require_once '../src/QueryPath/QueryPath.php'; - -// If you have the Zip lib combiled in: -//$file = 'zip://openoffice.odt#content'; -// Example for systems w/o zip lib: -$file = 'tmp/content.xml'; -$doc = qp($file); -print 'Contents:' . PHP_EOL; - -// Show the "outline": all of the heading items: -foreach ($doc->find('text|h') as $header) { - $style = $header->attr('text:style-name'); - $attr_parts = explode('_', $style); - $level = array_pop($attr_parts); - $out = str_repeat(' ', $level) . '- ' . $header->text(); - print $out . PHP_EOL; -} - -// This is a fairly sophisticated selector. It gets the first -// <text:list text:style-name="L1"> match and then gets the -// <text:p text:style-name="P1"> elements. That is the syntax for -// ODT lists. Not elegant.... -$selector = 'text|list[text|style-name="L1"]:first text|p[text|style-name="P1"]'; - -print PHP_EOL . "Bullet List" . PHP_EOL; -foreach ($doc->top()->find($selector) as $bullet) { - print ' * ' . $bullet->text() . PHP_EOL; -} - -print PHP_EOL . "Ordered List" . PHP_EOL; -$i = 0; -foreach ($doc->top()->find('text|list[text|style-name="L2"]:first text|p[text|style-name="P2"]') as $bullet) { - print ' ' . (++$i) . '. ' . $bullet->text() . PHP_EOL; -} diff --git a/examples/out.svg b/examples/out.svg deleted file mode 100644 index ee185792..00000000 --- a/examples/out.svg +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0"?> -<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" width="800" height="600" id="test"> - <desc>Created by QueryPath.</desc> -<rect id="first" x="2" y="2" width="40" height="60" fill="navy"/><rect id="second" x="15" y="4" width="40" height="60" fill="red"/></svg> diff --git a/examples/parse_php.php b/examples/parse_php.php deleted file mode 100644 index c2b6dc6e..00000000 --- a/examples/parse_php.php +++ /dev/null @@ -1,30 +0,0 @@ -<?php -/** - * Parsing PHP with QueryPath - * - * This file contains an example of how QueryPath can be used - * to parse a PHP file. Any well-formed XML or HTML document can be parsed. Since - * PHP tags are contained inside of processor instructions, an XML parser can - * correctly parse such a file into a DOM. Consequently, you can use QueryPath - * to read, modify, and traverse PHP files. - * - * This example illustrates how such a file can be parsed and manipulated. - * - * - * @author M Butcher <matt@aleph-null.tv> - * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. - */ -?> -<html> -<head> - <title>Parse PHP from QueryPath - - -text(); -?> - - \ No newline at end of file diff --git a/examples/parsing-php-source/index.php b/examples/parsing-php-source/index.php new file mode 100644 index 00000000..90171e70 --- /dev/null +++ b/examples/parsing-php-source/index.php @@ -0,0 +1,93 @@ + + * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. + * @see https://www.php.net/manual/en/language.basic-syntax.phpmode.php + */ + +require_once __DIR__ . '/../../vendor/autoload.php'; + +echo '

    Parsing a PHP template

    '; + +try { + /* + * Parse the template. htmlqp() is used rather than qp() because the file is + * HTML rather than well-formed XML. + * + * Note that the file is read, not executed - QueryPath sees the PHP source + * exactly as it sits on disk. + */ + $template = htmlqp(__DIR__ . '/template.php'); + + echo '

    Reading the markup

    '; + + printf( + '

    The template is titled %s and has %d list item(s).

    ', + htmlspecialchars($template->find('title')->text()), + $template->top()->find('#menu li')->count() + ); + + /* + * PHP blocks survive parsing as processing instruction nodes, so they can be + * located with XPath and inspected like any other node. + * + * The `data` property of the node holds the PHP source. libxml stores the + * trailing "?" of the closing tag as part of that data and puts the ">" back + * on when the document is written out, so trim it off before displaying it. + */ + echo '

    The PHP blocks in the template

    '; + + $blocks = $template->top()->xpath('//processing-instruction()'); + + echo '
      '; + + foreach ($blocks as $block) { + $code = rtrim(trim($block->get(0)->data), '?'); + + echo '
    1. ' . htmlspecialchars(trim($code)) . '
    2. '; + } + + echo '
    '; + + /* + * Because it is a normal DOM, the template can be rewritten too. Here a new + * menu item is added and the heading is retitled. + * + * Use writeHTML() (or writeXML()) to serialize a template containing PHP + * blocks - it hands the document back to libxml, which restores the closing + * "?>" correctly. Capturing it with an output buffer makes it easy to send + * the result somewhere other than standard output, such as back to disk with + * file_put_contents(). + */ + echo '

    Rewriting the template

    '; + + ob_start(); + + $template->top() + ->find('#menu') + ->append('
  • Contact
  • ') + ->top() + ->find('h1') + ->text('A rewritten template') + ->top() + ->writeHTML(); + + $rewritten = ob_get_clean(); + + echo '
    ' . htmlspecialchars(trim($rewritten)) . '
    '; +} catch (\QueryPath\Exception $e) { + // Handle QueryPath exceptions + echo $e->getMessage(); + exit(1); +} diff --git a/examples/parsing-php-source/template.php b/examples/parsing-php-source/template.php new file mode 100644 index 00000000..05a095c9 --- /dev/null +++ b/examples/parsing-php-source/template.php @@ -0,0 +1,22 @@ + + + + + A PHP template + + +

    + + + + +
    +

    +

    +
    + + + diff --git a/examples/parsing-rss-feed/index.php b/examples/parsing-rss-feed/index.php new file mode 100644 index 00000000..6dae98e5 --- /dev/null +++ b/examples/parsing-rss-feed/index.php @@ -0,0 +1,66 @@ + + * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. + */ +require_once __DIR__ . '/../../vendor/autoload.php'; + +// The URL of the remote RSS feed. +$remote = 'https://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss'; + +/* + * Many hosts - Wikipedia among them - reject requests that do not identify + * themselves, so send a User-Agent along with the request. + * + * QueryPath accepts a stream context under the 'context' option and hands it to + * the underlying stream wrapper, which is how any HTTP detail (headers, method, + * timeouts, proxies) gets configured. + */ +$context = stream_context_create([ + 'http' => [ + 'header' => 'User-Agent: QueryPath (+https://github.com/GravityPDF/querypath)', + ], +]); + +try { + // We will write the results into this document. + $qp = html5qp(\QueryPath\QueryPath::HTML5_STUB, 'title') + ->text('New Wikipedia Pages') + ->top('body') + ->append('

    New Wikipedia Pages

    ') + ->append('
      ') + ->children('ul'); + + // Load the remote document and loop through all the items. + foreach (qp($remote, 'channel>item', ['context' => $context]) as $item) { + // Get title and link. + $title = $item->find('title')->text(); + $link = $item->find('link')->text(); + + $list = html5qp('
    • ', 'li') + ->append('') + ->find('a') + ->attr('href', $link) + ->text($title); + + // Add it to the output document. + $qp->append($list->top()->innerHTML5()); + } + + // Write the results. + $qp->writeHTML5(); +} catch (Exception $e) { + echo $e->getMessage(); + exit(1); +} diff --git a/examples/parsing-xml-from-url-and-dynamically-generating-html/index.php b/examples/parsing-xml-from-url-and-dynamically-generating-html/index.php new file mode 100644 index 00000000..68287afc --- /dev/null +++ b/examples/parsing-xml-from-url-and-dynamically-generating-html/index.php @@ -0,0 +1,145 @@ +' . htmlentities($name) . '
      '; +} + +/** + * Fetch a remote XML document and parse it into a DOMDocument. + * + * QueryPath will happily fetch a URL for you - `qp($url)` is all it takes, and + * a stream context passed as the `context` option covers most HTTP needs. + * + * Doing the two steps by hand, as here, buys two things that matter when the + * document is coming off somebody else's server: + * + * - A User-Agent header. Plenty of hosts reject requests that do not send one. + * - Recovery mode. Real-world feeds are sometimes truncated or malformed, and + * `DOMDocument::$recover` salvages the part that did parse instead of + * throwing the whole response away. (At the time of writing jQuery's own + * api.xml is served truncated, so without this the example returns nothing.) + * + * QueryPath accepts the resulting DOMDocument directly. + * + * @param string $url + * + * @return \DOMDocument + */ +function fetchXML(string $url): \DOMDocument +{ + $context = stream_context_create([ + 'http' => [ + 'header' => 'User-Agent: QueryPath (+https://github.com/GravityPDF/querypath)', + ], + ]); + + $xml = file_get_contents($url, false, $context); + + if ($xml === false) { + throw new RuntimeException('Could not fetch ' . $url); + } + + $document = new DOMDocument(); + $document->recover = true; + + /* Collect libxml's complaints rather than letting them reach the output. */ + $previous = libxml_use_internal_errors(true); + $document->loadXML($xml); + libxml_clear_errors(); + libxml_use_internal_errors($previous); + + return $document; +} + +try { + // The document skeleton + $qp = html5qp(__DIR__ . '/template.html', 'body'); + + $key = $_GET['key'] ?? ''; + + // Only display jQuery methods from these categories + $categories = [ + 'traversing/tree-traversal' => 'Tree Traversal', + 'selectors/child-filter-selectors' => 'Child Filter', + 'selectors/attribute-selectors' => 'Attribute', + 'selectors/content-filter-selector' => 'Content Filter', + 'selectors/basic-filter-selectors' => 'Basic Filter', + 'selectors/hierarchy-selectors' => 'Hierarchy', + 'selectors/basic-css-selectors' => 'Basic', + 'traversing/filtering' => 'Filtering', + 'traversing/miscellaneous-traversal' => 'Miscellaneous Traversing', + 'manipulation/dom-insertion-outside' => 'DOM Insertion, Outside', + 'manipulation/dom-insertion-inside' => 'DOM Insertion, Inside', + 'manipulation/style-properties' => 'Style Properties', + ]; + + $jquery = []; + + // Search through the xml file to find all entries of jQuery entities + foreach (qp(fetchXML('https://api.jquery.com/resources/api.xml'), 'entry') as $entry) { + foreach ($entry->find('category') as $item) { + $category = $categories[$item->attr('slug')] ?? ''; + if ($category) { + $jquery[$entry->attr('name')] = [ + 'longdesc' => $entry->find('longdesc')->innerXML(), + 'name' => sprintf('%s: %s', $category, $entry->attr('name')), + ]; + + break; + } + } + } + + // Map the keys & sort them + $jqueryKeys = array_keys($jquery); + sort($jqueryKeys); + + $links = array_map('addClasses', $jqueryKeys); + // Add the keys to the nav bar + $sidebar = $qp->find('#leftbody'); + foreach ($links as $link) { + $sidebar->append($link); + } + + // Add the description to the main window if the key exists + $key = isset($jquery[$key]) ? $key : $jqueryKeys[0]; + + $qp->top()->find('#rightfunction')->text('Function: ' . ucfirst($key)); + $qp->top()->find('#rightdesc')->remove(); + $qp->top()->find('#righttitle')->text('jQuery Documentation'); + $qp->top()->find('#righttext')->append($jquery[$key]['longdesc']); + + $qp->top()->find('#current-year')->text(date('Y')); + + // Write the document + $qp->writeHTML5(); +} catch (\QueryPath\Exception $e) { + // Handle QueryPath exceptions + echo $e->getMessage(); + exit(1); +} catch (RuntimeException $e) { + // Handle a failed HTTP request + echo $e->getMessage(); + exit(1); +} diff --git a/examples/parsing-xml-from-url-and-dynamically-generating-html/template.html b/examples/parsing-xml-from-url-and-dynamically-generating-html/template.html new file mode 100644 index 00000000..5a06fc4e --- /dev/null +++ b/examples/parsing-xml-from-url-and-dynamically-generating-html/template.html @@ -0,0 +1,111 @@ + + + + + Example: Parsing XML from a URL and inserting into a HTML Template File + + + + +
      +
      +
      +
      +
      +
      +
      +
      +
      +
      + + + \ No newline at end of file diff --git a/examples/querypath.xml b/examples/querypath.xml deleted file mode 100644 index f16e4beb..00000000 --- a/examples/querypath.xml +++ /dev/null @@ -1,236 +0,0 @@ - - - - - Add constructs a new element at the specified location in the document. - - - -
        -
      • list item 1
      • -
      • list item 2
      • -
      • list item 3
      • -
      -

      a paragraph

      - - $('li').add('

      new paragraph

      ') - .css('background-color', 'red'); -
      -
      - - test.html - - - - -

      Hello

      - Hello Again - - -
      - - test.php - - require_once 'QueryPath.php'; - - htmlqp('test.html') - $qp->append('

      new paragraph

      ') - ->find('p') - ->css('background-color', 'red') - ->top() - ->writeXHTML(); -
      -
      -
      - - - It's important to note that this method does not replace a class. It simply adds the class, appending it to any which may already be assigned to the elements. - - More than one class may be added at a time, separated by a space, to the set of matched elements. - - - This method is often used with .removeClass() to switch elements' classes from one to another. - - $('p').addClass('myClass yourClass'); - - $('p').removeClass('myClass noClass').addClass('yourClass'); - - $('ul li:last').addClass(function() { - return 'item-' + $(this).index(); - }); - - Given an unordered list with five li elements, this example adds the class "item-4" to the last li. - - - test.html - - - - -

      Hello

      - Hello Again - - -
      - - test.php - - require_once 'QueryPath.php'; - - htmlqp('test.html') - $qp->append('
      ') - ->find('div') - ->addClass('testing one two three') - ->top() - ->writeXHTML(); -
      -
      -
      - - - The selector expression preceding the method is the container after which the content is inserted. - - - This method is often used with .removeClass() to switch elements' classes from one to another. - -
      -

      Greetings

      -
      Hello
      -
      Goodbye
      -
      - We can create content and insert it after several elements at once: - $('.inner').after('

      Test

      '); - We can also select an element on the page and insert it after another: - $('.container').after($('h2')); - .after() will also work on disconnected DOM nodes. For example, given the following code: - $('
      ').after('

      '); -
      - - - test.html - - - - -

      Hello

      - Hello Again - - -
      - - test.php - - require_once 'QueryPath.php'; - - htmlqp('test.html') - $qp->append('

      First P

      Second P

      ') - ->children('p') - ->after('

      new paragraph

      ') - ->top() - ->writeHTML(); -
      -
      - - - - - - - -
      -

      Greetings

      -
      Hello
      -
      Goodbye
      -
      -
      -
      - - test.html - - - - -

      Hello

      - Hello Again - - -
      - - test.php - - require_once 'QueryPath.php'; - - htmlqp('test.html') - $qp->append('

      First P

      Second P

      ') - ->children('p') - ->after('

      new paragraph

      ') - ->top() - ->writeHTML(); -
      -
      -
      - - - Add a yellow background to a span and to all of the children of an element. - - - - - - - - - -

      Hello

      - Hello Again -
        -
      • I
      • -
      • II -
          -
        • A
        • -
        • B -
            -
          • 1
          • -
          • 2
          • -
          • 3
          • -
          -
        • -
        • C
        • -
        -
      • -
      • III
      • -
      - - -
      -
      - - test.html - - - - -

      Hello

      - Hello Again - - -
      - - test.php - - require_once 'QueryPath.php'; - - htmlqp('test.html') - ->find('span') // Point querypath to the span - ->css('background', 'yellow') // Add the CSS - ->top() // Point QueryPath to the top of the page - ->writeXHTML(); // Write HTML, notice it does not use print - -
      -
      - \ No newline at end of file diff --git a/examples/quickstart-guide.md b/examples/quickstart-guide.md index da1cfde2..85b03a96 100644 --- a/examples/quickstart-guide.md +++ b/examples/quickstart-guide.md @@ -1,113 +1,187 @@ # QueryPath QuickStart -This short guide is intended to help you get started with QueryPath 3. +A short guide to getting started with QueryPath. -## Using QueryPath in Your Project +## Installing -To use QueryPath inside of your own application, you will need to make sure that PHP can find the QueryPath library. There are a few possible ways of doing this. The first is to use an autoloader. The second is to include QueryPath manually. We'll look briefly at each. +QueryPath is installed with [Composer](https://getcomposer.org): -### Autoloaders and QueryPath - -In recent time, PHP has standardized a method of automatically importing classes by name. This is often called [PSR-0 autoloading](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md). Symfony, Composer, and many other PHP projects use PSR-0 autoloaders, and QueryPath should work with those. In addition, QueryPath has its own autoloader in `qp.php`. +```bash +composer require gravitypdf/querypath +``` -To use QueryPath's autoloader, all you need to do is include `qp.php`. This will detect if another autoloader is already in place, and if not, it will configure it's own autoloader: +Include Composer's autoloader and you are ready to go: -```{php} +```php text(); +require 'vendor/autoload.php'; -print htmlqp('http://technosophos.com', 'title')->text(); -?> +echo html5qp('https://example.com', 'title')->text(); ``` -The above illustrates the requiring of QueryPath's autoloader. Note that in that case we don't need to do anything else to get the `QueryPath` class or the `htmlqp()` functions. +The three global functions – `qp()`, `htmlqp()`, and `html5qp()` – come from +`src/qp_functions.php`, which Composer loads for you through its `files` +autoloader. There is nothing else to include. -QueryPath also ships with [Composer](http://getcomposer.org) support. Composer provides PSR-0 autoloading. To use Composer's autoloader, you can do this: +Each function is a thin wrapper around a static factory, so the object-oriented +form is always available if you prefer it, or if a function of the same name is +already defined elsewhere in your application: -```{php} -text(); +## A simple example -// THIS DOESN'T WORK! -// print htmlqp('http://technosophos.com', 'title')->text(); -?> +```php +text(); ``` -Notice, though, that the `qp()` and `htmlqp` functions *will not work* with this method. Why? Because PHP's autoloader does not know about functions. It operates on classes only. So you can use QueryPath's Object-Oriented API (`QueryPath::with()`, `QueryPath::withHTML()`, `QueryPath::withXML()`), but not the `qp()` and `qphtml()` functions. If you want to use those, too, simply include `qp.php`: +That one line does three things: -```{php} - tr:odd td > a:first-of-type` works just as well. If you would + rather use XPath, there is an `xpath()` method. +3. **Reads a value.** `text()` returns the text content of the matches, or an + empty string when nothing matched. -print QueryPath::withHTML('http://technosophos.com', 'title')->text(); +From there, the API mirrors jQuery. There are methods for traversing (`find()`, +`top()`, `children()`, `next()`, `prev()`, `parents()`), for filtering (`filter()`, +`filterCallback()`, `map()`, `eq()`, `not()`), and for modifying a document +(`append()`, `prepend()`, `before()`, `after()`, `attr()`, `css()`, `addClass()`, +`text()`, `remove()`). Almost all of them return a `DOMQuery`, so calls chain. -// This works because qp.php was imported -print htmlqp('http://technosophos.com', 'title')->text(); -?> -``` +## HTML vs XML -## A Simple Example +QueryPath originally made no distinction between HTML and XML. In practice, HTML +cannot be parsed or serialized as though it were XML, so the two now have +separate entry points: -So far, we have seen a few variations of the same program. Let's learn what it does. Here's the program: +* **`html5qp()` / `QueryPath::withHTML5()`** – parses with masterminds/html5. + This is the recommended choice for anything that is HTML. +* **`htmlqp()` / `QueryPath::withHTML()`** – forces libxml's HTML parser and makes + a number of adjustments to accommodate common HTML breakages. Use it for + pre-HTML5 documents. +* **`QueryPath::withXML()`** – forces XML parsing. +* **`qp()` / `QueryPath::with()`** – inspects the document and guesses, favouring + XML slightly. It decides from the file extension, the XML declaration, and any + options passed in. Because it guesses, an XML *string* handed to `qp()` should + begin with ``. -```{php} -text(); +Empty documents to build on are available as constants: +`QueryPath::HTML5_STUB`, `QueryPath::HTML_STUB`, and `QueryPath::XHTML_STUB`. -print htmlqp('http://technosophos.com', 'title')->text(); -?> +## Character encoding + +XML expects UTF-8. Plenty of HTML is encoded as something else – often +ISO-8859-1 – and web servers regularly report one character set while serving +another. QueryPath tries to convert documents automatically using PHP's character +detection, but it does sometimes guess wrong. When it does, pass the encoding +explicitly in the `$options` array: + +```php + 'ISO-8859-1']); ``` -This does the same thing two different ways. Let's look at line 3: +## Remote documents -```{php} +`qp()` and `htmlqp()` fetch URLs through PHP's stream wrappers, so a stream +context passed as the `context` option controls the request – headers, method, +timeouts, proxies, and so on: + +```php text(); -?> +$context = stream_context_create([ + 'http' => [ + 'header' => 'User-Agent: My Application', + ], +]); + +qp('https://example.com/feed.xml', 'item', ['context' => $context]); ``` -This line does three things: +`html5qp()` fetches URLs through masterminds/html5, which does not take a stream +context. To control that request, fetch the page yourself and pass the markup to +`html5qp()` as a string. -1. It loads and parses the HTML document it finds at `http://techosophos.com`. QueryPath can load documents locally and remotely. It can also load strings of HTML or XML, as well as `SimpleXML` objects and `DOMDocument` objects. It should be easy to get your HTML or XML loaded into QueryPath. -2. It performs a search for the tag named `title`. QueryPath uses CSS 4 Selectors (as the current draft stands) as a query language -- just like jQuery and CSS. (If you prefer XPath, check out the `xpath()` method on QueryPath). Of course, `title` is a very basic selector. You can do more advanced selectors like `#bar-one table>tr:odd td>a:first-of-type()`, which looks for the element with ID `bar-one` and then fetches every odd row from its table, then from each cell in the row, it finds the first hyperlink. -3. Finally, the example calls `text()`, which will fetch the text content of the first element it's found (in this case, the `title` tag in the HTML head). If not title is found, this will return an empty string. Otherwise it will return the text of that tag. +## The examples -QueryPath has well over 60 methods like `text()`. Some are for navigating, like `top()`, `children()`, `next()`, and `prev()`. Some are for manipulating the parts of an HTML or XML element, like `attar()`. Others are for doing sophisticated finding and filtering operations (`find()`, `filter()`, `filterCallback()`, `map()`, and so on). And, of course, there are methods for modifying the document (`append()`, `before()`, `after()`, `attr()`, `text()`, and many more). +Every example in this directory is runnable on its own: -The goal of QueryPath is to make it easy for you to process XML and HTML documents. There may be a lot of methods to learn (just like jQuery), but those methods are there to make your life simpler. +```bash +composer install +php examples/hello-world/index.php +``` -## HTML vs XML +Most of them print HTML, so they also work under a web server: + +```bash +php -S localhost:8000 -t examples +``` + +### The basics + +| Example | What it covers | +|---------|----------------| +| [hello-world](hello-world/index.php) | The smallest useful program: parse, select, read, and modify. | +| [basic-manipulation-filter-and-retrieval](basic-manipulation-filter-and-retrieval/index.php) | Selecting, filtering, and manipulating HTML and XML side by side. | +| [iterating-over-matches](iterating-over-matches/index.php) | Five ways to loop over a set of matches, and how changes made in a loop stick. | +| [filtering-by-text-content](filtering-by-text-content/index.php) | `:contains()` and `filterCallback()` for matching on content rather than structure. | -When QueryPath was first introduced, it did not distinguish between XML and HTML documents. At that time, momentum was behind XHTML, and it looked like the future was XML. But over time, it has become abundantly clear that HTML documents cannot be treated as XML during parsing and processing, or during output. +### Building documents -So there are now separate parser functions for HTML and XML -- as well as a generic parser function that inspects the document and attempts to determine whether it is XML or HTML: +| Example | What it covers | +|---------|----------------| +| [create-html-document](create-html-document/index.php) | Building a full HTML document in a single chain. | +| [create-xml-document](create-xml-document/index.php) | Building XML with `wrap()`, `before()`, `after()`, and friends. | +| [create-svg-document](create-svg-document/index.php) | Generating an SVG image from an XML stub. | +| [generating-rss-feed](generating-rss-feed/index.php) | Merging data into stub documents to render a feed. | -* `QueryPath::withXML()`: This *only* handles XML documents. If you give it an HTML document, it will attempt to force XML parsing on that document. -* `htmlqp()`, `QueryPath::withHTML()`: This will force QueryPath to use the HTML parser. it will also make a number of adjustments to QueryPath to accommodate common HTML breakages. -* `qp()`, `QueryPath::with()`: This will attempt to guess whether the document is XML or HTML. In general, it favors XML slightly. Guessing may be done by… - - File extension - - XML declaration - - The suggestions made by any options passed into the document +### Working with remote data -###… And Character Encoding +| Example | What it covers | +|---------|----------------| +| [parsing-rss-feed](parsing-rss-feed/index.php) | Fetching and parsing a remote RSS feed, with a stream context. | +| [remote-filter-and-retrieval](remote-filter-and-retrieval/index.php) | Scraping an HTML page and pulling values out of it. | +| [curl-xml-filter-and-retrieval](curl-xml-filter-and-retrieval/index.php) | Two chained REST requests with cURL, and attribute selectors over the XML. | +| [parsing-xml-from-url-and-dynamically-generating-html](parsing-xml-from-url-and-dynamically-generating-html/index.php) | Reading a remote XML API and rendering it into an HTML template. | +| [http-stream-xml-namespaces-and-linked-data](http-stream-xml-namespaces-and-linked-data/index.php) | Stream contexts, XML namespaces, and Linked Data from DBpedia. | +| [sparql-endpoint-query](sparql-endpoint-query/index.php) | Querying a SPARQL endpoint and rendering the results as a table. | -XML suggests that all documents be encoded as UTF-8. Most HTML documents are encoded using one of the ISO specifications (typically ISO-8859-1). And web servers are often misconfigured to report that documents are using one character set when they are actually using another. +### Parsing other formats -To work around all of these issues, QueryPath attempts to convert documents automatically. It does this using PHP's internal character detection libraries. But sometimes it guesses wrong. You can adjust this feature manually by passing in language settings in the `$options` array. See the documentation on `qp()` for details. +| Example | What it covers | +|---------|----------------| +| [basic-docx-parser](basic-docx-parser/index.php) | Reading a Word `.docx` file out of its ZIP archive. | +| [basic-odt-parser](basic-odt-parser/index.php) | Reading an OpenDocument `.odt` file via the `zip://` stream wrapper. | +| [parsing-php-source](parsing-php-source/index.php) | Traversing and rewriting a PHP template, PHP blocks and all. | +Several of these call live third-party services, which change their markup, +rate-limit, and occasionally go down. An example that reaches across the network +may need its selectors adjusted from time to time; the ones that read local +fixtures will always work. ## Where to go from here -* [QueryPath.org](http://querypath.org) has pointers to other resources. -* [The API docs](http://api.querypath.org) have detailed explanations of every single part of QueryPath. +* The source is documented inline; `src/Helpers/` holds most of the jQuery-style + methods. +* Issues and discussions live on [GitHub][repo]. - \ No newline at end of file +[html5]: https://github.com/Masterminds/html5-php +[repo]: https://github.com/GravityPDF/querypath diff --git a/examples/remote-filter-and-retrieval/index.php b/examples/remote-filter-and-retrieval/index.php new file mode 100644 index 00000000..c1b5ab04 --- /dev/null +++ b/examples/remote-filter-and-retrieval/index.php @@ -0,0 +1,100 @@ +Urban Dictionary Random Word Generator'; + +try { + /* Urban Dictionary paginates its front page; the first hundred always exist. */ + $page = random_int(1, 100); + $url = 'https://www.urbandictionary.com/?page=' . $page; + + /* + * Identify the request with a User-Agent - many hosts, Urban Dictionary + * included, reject requests that do not send one. + * + * Note that html5qp() handed a URL fetches it through masterminds/html5, + * which does not take a stream context. Fetching the page first and passing + * the markup as a string keeps control of the request. (qp() and htmlqp() + * do accept a stream context, via the 'context' option - see the + * parsing-rss-feed example.) + */ + $context = stream_context_create([ + 'http' => [ + 'header' => 'User-Agent: QueryPath (+https://github.com/GravityPDF/querypath)', + ], + ]); + + // Load a random page from the Urban Dictionary + $html = file_get_contents($url, false, $context); + + if ($html === false) { + echo 'Could not reach the Urban Dictionary.'; + exit(1); + } + + $qp = html5qp($html); + + /* + * Count the definitions on the page rather than assuming how many there are. + * A site can change how much it lists per page at any time, and picking an + * index past the end of the match set gets you an empty string rather than + * an error. + */ + $total = $qp->find('.word')->count(); + + if ($total === 0) { + echo 'Found no definitions on the page - the site markup has probably changed.'; + exit(1); + } + + // Pick one of them at random + $word = random_int(0, $total - 1); + + // Get the word + printf( + '

      Word %d of %d: %s

      ', + $word + 1, + $total, + $qp->top() + ->find('.word') + ->eq($word) + ->text() + ); + + // Get the definition that goes with it + $definition = $qp->top() + ->find('.meaning') + ->eq($word) + ->text(); + + /* + * Guard the assumption that a .word has a matching .meaning. text() returns + * an empty string when a selector matches nothing, so without this an + * unexpected page layout would look like a definition that happens to be + * blank rather than a broken selector. + */ + if (trim($definition) === '') { + echo 'Found a word with no definition - the site markup has probably changed.'; + exit(1); + } + + echo 'Definition: ' . $definition; + + printf('

      Source: %1$s

      ', htmlspecialchars($url)); +} catch (\QueryPath\Exception $e) { + // Handle QueryPath exceptions + echo $e->getMessage(); + exit(1); +} catch (Exception $e) { + // Handle the random_int() exception + echo $e->getMessage(); + exit(1); +} diff --git a/examples/rss.php b/examples/rss.php deleted file mode 100644 index c17d3b7c..00000000 --- a/examples/rss.php +++ /dev/null @@ -1,109 +0,0 @@ - - * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. - */ - -require_once '../src/QueryPath/QueryPath.php'; - -// This is the stub RSS document. -$rss_stub = ' - - - - - - en - QueryPath - - -'; - -// This is the stub RSS element. -$rss_item_stub = ' - - Untitled - - - - - - - -'; - -// Here are some dummy items. For the same of -// simplicity, we are just using a nested array. Of -// course, this could be a database lookup or whatever. -$items = [ - [ - 'title' => 'Item 1', - 'link' => 'http://example.com/item1', - 'description' => 'This has embedded HTML', - 'comments' => 'http://example.com/item1/comments', - 'category' => 'Some Term', - 'pubDate' => date('r'), - 'guid' => '123456-789', - ], - [ - 'title' => 'Item 2', - 'link' => 'http://example.com/item2', - 'description' => 'This has embedded HTML', - 'comments' => 'http://example.com/item2/comments', - 'category' => 'Some Other Term', - 'pubDate' => date('r'), - 'guid' => '123456-790', - ], -]; - -// The main QueryPath, which holds the channel. -$qp = qp($rss_stub, 'title') - ->text('A QueryPath RSS Feed') - ->next('link')->text('http://example.com') - ->next('description')->text('QueryPath: Find your way.') - ->parent(); - -// For each element in the array above, we create a new -// QueryPath and then populate the XML fragment with data. -foreach ($items as $item) { - // Begin with the stub RSS item, with title currently selected. - $qpi = qp($rss_item_stub, 'title') - // Add a title. - ->text($item['title']) - // Add a link. Note that we are giving no args to next() for the - // sake of simplicity. - ->next()->text($item['link']) - // Go to next element and add a description. Note that the text() - // call will automatically encode HTML. < will become < and so on. - ->next()->text($item['description']) - // Go on down the list... - ->next()->text($item['comments']) - ->next()->text($item['category']) - ->next()->text($item['pubDate']) - ->next()->text($item['guid']); - - // Now we append it. - $qp->append($qpi->top()); -} - -// If we were running this on a server, we would need to set the content -// type: -// header('Content-Type: application/rss+xml'); - -// Write the outpt as XML. -$qp->writeXML(); diff --git a/examples/simple_example.php b/examples/simple_example.php deleted file mode 100644 index 61c31d8f..00000000 --- a/examples/simple_example.php +++ /dev/null @@ -1,37 +0,0 @@ - - * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. - * @see qp() - * @see QueryPath::find() - * @see QueryPath::writeHTML() - * @see html.php - * @see https://fedorahosted.org/querypath/wiki/QueryPathTutorial The Official Tutorial - */ -require_once '../src/QueryPath/QueryPath.php'; -qp(QueryPath::HTML_STUB)->find('body')->text('Hello World')->writeHTML(); - -$qp = htmlqp(QueryPath::HTML_STUB, 'body'); - - -$qp->append('

      Hello

      Goodbye

      ') - ->children('p') - ->after('

      new paragraph

      '); - -echo ($qp->find('p')->children('p')->html()) ? 'print' : 'dont print'; - -// ->writeHTML(); diff --git a/examples/sparql-endpoint-query/index.php b/examples/sparql-endpoint-query/index.php new file mode 100644 index 00000000..53836893 --- /dev/null +++ b/examples/sparql-endpoint-query/index.php @@ -0,0 +1,124 @@ + + * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. + * @see https://www.w3.org/TR/rdf-sparql-XMLres/ + * @see https://dbpedia.org/sparql + */ + +require_once __DIR__ . '/../../vendor/autoload.php'; + +/* DBpedia's public SPARQL endpoint. */ +$endpoint = 'https://dbpedia.org/sparql'; + +/* Ask for every label DBpedia holds for The Beatles, and the language of each. */ +$sparql = ' + PREFIX rdfs: + + SELECT ?label (lang(?label) AS ?language) + WHERE { + rdfs:label ?label + } + ORDER BY ?language +'; + +/* + * Build the request. The `format` parameter is what asks the endpoint for XML + * rather than its default HTML result browser. + */ +$url = $endpoint . '?' . http_build_query([ + 'query' => $sparql, + 'format' => 'application/sparql-results+xml', +]); + +echo '

      Querying a SPARQL endpoint

      '; + +echo '

      The query

      '; +echo '
      ' . htmlspecialchars(trim($sparql)) . '
      '; + +try { + /* + * Retrieve and parse the response in one step - qp() will fetch a URL just + * as happily as it reads a local file or a string. + */ + $qp = qp($url); + + /* + * A SPARQL result document has two halves: + * + * one per column + * one per row, each holding a per cell + * + * Start with the column names. + */ + $columns = []; + + foreach ($qp->find('head > variable') as $variable) { + $columns[] = $variable->attr('name'); + } + + if (count($columns) === 0) { + echo '

      The endpoint returned no columns.

      '; + exit; + } + + /* + * Then the rows. Bindings are keyed by name rather than by position, and a + * row may omit a binding entirely when the value is unbound, so index each + * row by name instead of relying on the order the cells arrive in. + */ + $rows = []; + + foreach ($qp->top()->find('results > result') as $result) { + $row = []; + + foreach ($result->children('binding') as $binding) { + $row[$binding->attr('name')] = $binding->text(); + } + + $rows[] = $row; + } + + echo '

      ' . count($rows) . ' result(s)

      '; + + /* Render the table. */ + echo ''; + + echo ''; + foreach ($columns as $column) { + echo ''; + } + echo ''; + + foreach ($rows as $row) { + echo ''; + + foreach ($columns as $column) { + /* An unbound value is simply missing from the row. */ + echo ''; + } + + echo ''; + } + + echo '
      ' . htmlspecialchars($column) . '
      ' . htmlspecialchars($row[$column] ?? '') . '
      '; +} catch (\QueryPath\Exception $e) { + // Handle QueryPath exceptions + echo $e->getMessage(); + exit(1); +} diff --git a/examples/sparql.php b/examples/sparql.php deleted file mode 100644 index b8dcceb0..00000000 --- a/examples/sparql.php +++ /dev/null @@ -1,84 +0,0 @@ - - * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. - * @see http://www.w3.org/2009/sparql/wiki/Main_Page - * @see http://dbpedia.org - * @see dbpedia.php - * @see musicbrainz.php - * @see http://drupal.org/project/querypath - */ - -require '../src/QueryPath/QueryPath.php'; - -// We are using the dbpedia database to execute a SPARQL query. - -// URL to DB Pedia's SPARQL endpoint. -$url = 'http://dbpedia.org/sparql'; - -// The SPARQL query to run. -$sparql = ' - PREFIX foaf: - PREFIX rdfs: - SELECT ?uri ?name ?label - WHERE { - ?uri foaf:name ?name . - ?uri rdfs:label ?label - FILTER (?name = "The Beatles") - FILTER (lang(?label) = "en") - } -'; - -// We first set up the parameters that will be sent. -$params = [ - 'query' => $sparql, - 'format' => 'application/sparql-results+xml', -]; - -// DB Pedia wants a GET query, so we create one. -$data = http_build_query($params); -$url .= '?' . $data; - -// Next, we simply retrieve, parse, and output the contents. -$qp = qp($url, 'head'); - -// Get the headers from the resulting XML. -$headers = []; -foreach ($qp->children('variable') as $col) { - $headers[] = $col->attr('name'); -} - -// Get rows of data from result. -$rows = []; -$col_count = count($headers); -foreach ($qp->top()->find('results>result') as $row) { - $cols = []; - $row->children(); - for ($i = 0; $i < $col_count; ++$i) { - $cols[$i] = $row->branch()->eq($i)->text(); - } - $rows[] = $cols; -} - -// Turn data into table. -$table = ''; -foreach ($rows as $row) { - $table .= ''; -} -$table .= '
      ' . implode('', $headers) . '
      '; - $table .= implode('', $row); - $table .= '
      '; - -// Add table to HTML document. -qp(QueryPath::HTML_STUB, 'body')->append($table)->writeHTML(); diff --git a/examples/svg.php b/examples/svg.php deleted file mode 100644 index 23e655a3..00000000 --- a/examples/svg.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. - */ - -require_once '../src/QueryPath/QueryPath.php'; - -// Let's stub out a basic SVG document. -$svg_stub = ' - - Created by QueryPath. -'; - -qp($svg_stub) - ->attr(['width' => 800, 'height' => 600]) - ->append('') - ->find('#second') - ->attr(['x' => 15, 'y' => 4, 'width' => 40, 'height' => 60, 'fill' => 'red']) - ->prev() - ->attr(['x' => 2, 'y' => 2, 'width' => 40, 'height' => 60, 'fill' => 'navy']) - ->writeXML(); diff --git a/examples/techniques.php b/examples/techniques.php deleted file mode 100644 index e47f5ddb..00000000 --- a/examples/techniques.php +++ /dev/null @@ -1,61 +0,0 @@ - - * @license LGPL (The GNU Lesser GPL) or an MIT-like license. - */ - -require '../src/QueryPath/QueryPath.php'; - -$demo = ' - -
    • Foo
    • -
    • Foo
    • -
    • Foo
    • -
    • Foo
    • -
    • Foo
    • - -'; - -$qp = qp($demo, 'data'); - -// Iterate over elements as DOMNodes: -foreach ($qp->get() as $li_ele) { - print $li_ele->tagName . PHP_EOL; // Prints 'li' five times. -} - -// Iterate over elements as QueryPath objects -foreach ($qp as $li_qp) { - print $li_qp->tag() . PHP_EOL; // Prints 'li' five times -} - -function callbackFunction($index, $element) -{ - print $element->tagName . PHP_EOL; -} - -// Iterate using a callback function -$qp->each('callbackFunction'); - -// Iterate using a Lambda-style function -$qp->eachLambda('return $item->tagName . PHP_EOL;'); - -// Loop through by index/count -for ($i = 0; $i < $qp->size(); ++$i) { - $domElement = $qp->get($i); - print $domElement->tagName . PHP_EOL; -} diff --git a/examples/testGrid.html b/examples/testGrid.html deleted file mode 100644 index 17378179..00000000 --- a/examples/testGrid.html +++ /dev/null @@ -1,47 +0,0 @@ - - - - -Example of 960 Grid Layout - - - - - - -
      -
      QueryPath
      -
      -
      -
      Learn More
      -

      Visit the Wiki to learn more.

      -
      -
      -
      Download Now
      -

      Get QueryPath 1.0.

      -
      -
      -
      -
      Tutorial
      -

      Learn the basics of QueryPath from the tutorial.

      -
      -
      QueryPath is a library for working with XML and HTML documents.
      -
      -
      Examples
      -

      Along with a complete set of API documents and unit tests, QueryPath includes examples showing how QueryPath can create HTML, XML, RSS, and even SVG.

      -
      -
      -
      -
      Using the Database
      -

      A database wrapper lets you integrate database queries right into the DOM. View the tutorial.

      -
      -
      -
      Using Templates
      -

      The template extension allows you to use pure HTML as a template language. View the tutorial.

      -
      -
      -
      Copyright (c) 2009, Matt Butcher.
      -
      -
      - - diff --git a/examples/xml.php b/examples/xml.php deleted file mode 100644 index 1293f29b..00000000 --- a/examples/xml.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * - * (A space was inserted above to prevent the documentation renderer from - * misinterpreting it.) - * - * @author M Butcher - * @license LGPL The GNU Lesser GPL (LGPL) or an MIT-like license. - */ - -require_once '../src/QueryPath/QueryPath.php'; - - -// Create a new XML document wrapped in a QueryPath. -// By default, it will point to the root element, -// -$record = qp('') - // Add a new last name inside of author. - ->append('Dostoyevsky') - // Select all of the children of . In this case, - // that is - ->children() - // Oh, wait... we wanted last name to be inside of a - // element. Use wrap to wrap the current element in something: - ->wrap('') - // And before last name, we want to add first name. - ->before('') - // Select first name - ->prev() - // Set the text of first name - ->text('Fyodor') - // And then after first name, add the patronymic - ->after('Fyodorovich') - // Now go back to the root element, the top of the document. - ->top() - // Add another tag -- origin. - ->append('Russia') - // turn the QueryPath contents back into a string. Since we are - // at the top of the document, the whole document will be converted - // to a string. - ->xml(); - -// Print our results. -print $record; diff --git a/src/DOMQuery.php b/src/DOMQuery.php index 8dde7d07..5f2eb870 100644 --- a/src/DOMQuery.php +++ b/src/DOMQuery.php @@ -1216,8 +1216,8 @@ public function xhtml($markup = null) * In getter mode, the first element wrapped by this DOMNode object will be * converted to an XML string and returned. * - * @param string|null $markup - * A string containing XML data. + * @param string|true|null $markup + * A string containing XML data. If true is passed, the XML declaration will be omitted. * * @return DOMQuery|string|null * If markup is passed in, a DOMQuery is returned. If no markup is passed @@ -1594,7 +1594,7 @@ public function __call($name, $arguments) /** * Get an iterator for the matches in this object. * - * @return Traversable + * @return Traversable * Returns an iterator. */ public function getIterator(): Traversable diff --git a/tests/QueryPath/ExampleRunner.php b/tests/QueryPath/ExampleRunner.php new file mode 100644 index 00000000..29724c93 --- /dev/null +++ b/tests/QueryPath/ExampleRunner.php @@ -0,0 +1,256 @@ + path map. + * + * @return array + */ + public static function all(): array + { + $examples = []; + + foreach (glob(self::examplesDir() . '/*/index.php') as $path) { + $examples[basename(dirname($path))] = $path; + } + + if (count($examples) === 0) { + throw new RuntimeException('No examples found in ' . self::examplesDir()); + } + + ksort($examples); + + return $examples; + } + + /** + * The examples that run without touching the network. + * + * @return array + */ + public static function offline(): array + { + return array_diff_key(self::all(), array_flip(self::NETWORK_EXAMPLES)); + } + + /** + * The examples that call a third-party service. + * + * @return array + */ + public static function network(): array + { + return array_intersect_key(self::all(), array_flip(self::NETWORK_EXAMPLES)); + } + + /** + * Run one example and report on how it went. + * + * @param string $path Path to the example's index.php. + * @param bool $offline Disable allow_url_fopen, so an example that is + * wrongly classified as offline fails loudly. + * + * @return array { + * + * @type bool $passed + * @type string $reason Empty when the example passed. + * @type string $output Combined stdout and stderr. + * @type int $status Exit code, or -1 if the example timed out. + * } + */ + public static function run(string $path, bool $offline = true): array + { + $timeout = $offline ? self::TIMEOUT_OFFLINE : self::TIMEOUT_NETWORK; + + $command = [PHP_BINARY, '-d', 'error_reporting=E_ALL', '-d', 'display_errors=1']; + + if ($offline) { + $command[] = '-d'; + $command[] = 'allow_url_fopen=0'; + } + + $command[] = $path; + + $result = self::execute($command, dirname($path), $timeout); + + return self::judge($result, $timeout); + } + + /** + * Decide whether a completed run counts as a pass. + * + * @param array $result + * @param int $timeout + * + * @return array + */ + private static function judge(array $result, int $timeout): array + { + $fail = function ($reason) use ($result) { + return array_merge($result, ['passed' => false, 'reason' => $reason]); + }; + + if ($result['status'] === -1) { + return $fail(sprintf('Timed out after %d seconds', $timeout)); + } + + if ($result['status'] !== 0) { + return $fail(sprintf('Exited with status %d', $result['status'])); + } + + if (preg_match(self::DIAGNOSTIC_PATTERN, $result['output'], $matches) === 1) { + return $fail('Emitted a PHP diagnostic: ' . trim($matches[0])); + } + + $length = strlen(trim($result['output'])); + + if ($length < self::MINIMUM_OUTPUT_BYTES) { + return $fail(sprintf( + 'Produced only %d bytes of output, expected at least %d', + $length, + self::MINIMUM_OUTPUT_BYTES + )); + } + + return array_merge($result, ['passed' => true, 'reason' => '']); + } + + /** + * Run a command, capturing its output and enforcing a timeout. + * + * @param array $command + * @param string $cwd + * @param int $timeout + * + * @return array + */ + private static function execute(array $command, string $cwd, int $timeout): array + { + $descriptors = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + + $pipes = []; + + /* + * proc_open() only accepts an array of arguments from PHP 7.4 onwards. + * Below that the command has to be escaped into a string by hand. + */ + $spec = PHP_VERSION_ID >= 70400 ? $command : implode(' ', array_map('escapeshellarg', $command)); + + $process = proc_open($spec, $descriptors, $pipes, $cwd); + + if (! is_resource($process)) { + throw new RuntimeException('Could not start ' . end($command)); + } + + fclose($pipes[0]); + + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + + $output = ''; + $deadline = time() + $timeout; + $timedOut = false; + $exitCode = 0; + + while (true) { + $output .= (string) stream_get_contents($pipes[1]); + $output .= (string) stream_get_contents($pipes[2]); + + $status = proc_get_status($process); + + /* + * Read the exit code from proc_get_status() rather than proc_close(). + * The first call that sees the process finish is the one that reaps + * it; proc_close() afterwards reports -1. + */ + if (! $status['running']) { + $exitCode = $status['exitcode']; + break; + } + + if (time() >= $deadline) { + $timedOut = true; + proc_terminate($process, 9); + break; + } + + /* Wait briefly rather than spinning on the pipes. */ + usleep(20000); + } + + /* Drain whatever was still buffered when the process ended. */ + $output .= (string) stream_get_contents($pipes[1]); + $output .= (string) stream_get_contents($pipes[2]); + + fclose($pipes[1]); + fclose($pipes[2]); + + proc_close($process); + + return [ + 'output' => $output, + 'status' => $timedOut ? -1 : $exitCode, + ]; + } +} diff --git a/tests/QueryPath/ExamplesTest.php b/tests/QueryPath/ExamplesTest.php new file mode 100644 index 00000000..185fc0ce --- /dev/null +++ b/tests/QueryPath/ExamplesTest.php @@ -0,0 +1,91 @@ + $path) { + $cases[$name] = [$name, $path]; + } + + return $cases; + } + + /** + * @dataProvider offlineExampleProvider + * + * @param string $name + * @param string $path + */ + public function testExampleRunsCleanly($name, $path) + { + $result = ExampleRunner::run($path, true); + + $this->assertTrue( + $result['passed'], + sprintf( + "The %s example did not run cleanly.\n\n%s\n\nOutput:\n%s", + $name, + $result['reason'], + trim($result['output']) === '' ? '(no output)' : trim($result['output']) + ) + ); + } + + /** + * Guards the network list itself: every name in it has to correspond to a + * real example, or the offline suite silently stops covering something. + */ + public function testNetworkExampleListIsAccurate() + { + $missing = array_diff(ExampleRunner::NETWORK_EXAMPLES, array_keys(ExampleRunner::all())); + + $this->assertSame( + [], + array_values($missing), + 'ExampleRunner::NETWORK_EXAMPLES names examples that do not exist: ' + . implode(', ', $missing) + ); + } + + /** + * Every example directory needs an index.php, otherwise it is not runnable + * and nothing above will notice it exists. + */ + public function testEveryExampleDirectoryIsRunnable() + { + $directories = glob(ExampleRunner::examplesDir() . '/*', GLOB_ONLYDIR); + + $withoutEntryPoint = []; + + foreach ($directories as $directory) { + if (! file_exists($directory . '/index.php')) { + $withoutEntryPoint[] = basename($directory); + } + } + + $this->assertSame( + [], + $withoutEntryPoint, + 'Example directories with no index.php: ' . implode(', ', $withoutEntryPoint) + ); + } +} diff --git a/tests/run-examples.php b/tests/run-examples.php new file mode 100644 index 00000000..83cf9032 --- /dev/null +++ b/tests/run-examples.php @@ -0,0 +1,87 @@ + $path) { + printf(' %-56s ', $name); + + /* Network examples are always run with networking available. */ + $isOffline = $offline || ! in_array($name, ExampleRunner::NETWORK_EXAMPLES, true); + + $result = ExampleRunner::run($path, $isOffline); + + if ($result['passed']) { + printf('ok (%d bytes)' . PHP_EOL, strlen(trim($result['output']))); + continue; + } + + echo 'FAILED' . PHP_EOL; + + $failures[$name] = $result; +} + +if (count($failures) === 0) { + printf(PHP_EOL . 'All %d example(s) passed.' . PHP_EOL, count($examples)); + exit(0); +} + +printf(PHP_EOL . '%d of %d example(s) failed:' . PHP_EOL, count($failures), count($examples)); + +foreach ($failures as $name => $result) { + printf(PHP_EOL . '--- %s ---' . PHP_EOL, $name); + echo $result['reason'] . PHP_EOL; + + $output = trim($result['output']); + + if ($output !== '') { + echo PHP_EOL . 'Output:' . PHP_EOL; + echo substr($output, 0, 2000) . PHP_EOL; + } +} + +exit(1);