ParserOutputTest.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939
  1. <?php
  2. use Wikimedia\TestingAccessWrapper;
  3. /**
  4. * @group Database
  5. * ^--- trigger DB shadowing because we are using Title magic
  6. */
  7. class ParserOutputTest extends MediaWikiLangTestCase {
  8. public static function provideIsLinkInternal() {
  9. return [
  10. // Different domains
  11. [ false, 'http://example.org', 'http://mediawiki.org' ],
  12. // Same domains
  13. [ true, 'http://example.org', 'http://example.org' ],
  14. [ true, 'https://example.org', 'https://example.org' ],
  15. [ true, '//example.org', '//example.org' ],
  16. // Same domain different cases
  17. [ true, 'http://example.org', 'http://EXAMPLE.ORG' ],
  18. // Paths, queries, and fragments are not relevant
  19. [ true, 'http://example.org', 'http://example.org/wiki/Main_Page' ],
  20. [ true, 'http://example.org', 'http://example.org?my=query' ],
  21. [ true, 'http://example.org', 'http://example.org#its-a-fragment' ],
  22. // Different protocols
  23. [ false, 'http://example.org', 'https://example.org' ],
  24. [ false, 'https://example.org', 'http://example.org' ],
  25. // Protocol relative servers always match http and https links
  26. [ true, '//example.org', 'http://example.org' ],
  27. [ true, '//example.org', 'https://example.org' ],
  28. // But they don't match strange things like this
  29. [ false, '//example.org', 'irc://example.org' ],
  30. ];
  31. }
  32. /**
  33. * Test to make sure ParserOutput::isLinkInternal behaves properly
  34. * @dataProvider provideIsLinkInternal
  35. * @covers ParserOutput::isLinkInternal
  36. */
  37. public function testIsLinkInternal( $shouldMatch, $server, $url ) {
  38. $this->assertEquals( $shouldMatch, ParserOutput::isLinkInternal( $server, $url ) );
  39. }
  40. /**
  41. * @covers ParserOutput::setExtensionData
  42. * @covers ParserOutput::getExtensionData
  43. */
  44. public function testExtensionData() {
  45. $po = new ParserOutput();
  46. $po->setExtensionData( "one", "Foo" );
  47. $this->assertEquals( "Foo", $po->getExtensionData( "one" ) );
  48. $this->assertNull( $po->getExtensionData( "spam" ) );
  49. $po->setExtensionData( "two", "Bar" );
  50. $this->assertEquals( "Foo", $po->getExtensionData( "one" ) );
  51. $this->assertEquals( "Bar", $po->getExtensionData( "two" ) );
  52. $po->setExtensionData( "one", null );
  53. $this->assertNull( $po->getExtensionData( "one" ) );
  54. $this->assertEquals( "Bar", $po->getExtensionData( "two" ) );
  55. }
  56. /**
  57. * @covers ParserOutput::setProperty
  58. * @covers ParserOutput::getProperty
  59. * @covers ParserOutput::unsetProperty
  60. * @covers ParserOutput::getProperties
  61. */
  62. public function testProperties() {
  63. $po = new ParserOutput();
  64. $po->setProperty( 'foo', 'val' );
  65. $properties = $po->getProperties();
  66. $this->assertEquals( $po->getProperty( 'foo' ), 'val' );
  67. $this->assertEquals( $properties['foo'], 'val' );
  68. $po->setProperty( 'foo', 'second val' );
  69. $properties = $po->getProperties();
  70. $this->assertEquals( $po->getProperty( 'foo' ), 'second val' );
  71. $this->assertEquals( $properties['foo'], 'second val' );
  72. $po->unsetProperty( 'foo' );
  73. $properties = $po->getProperties();
  74. $this->assertEquals( $po->getProperty( 'foo' ), false );
  75. $this->assertArrayNotHasKey( 'foo', $properties );
  76. }
  77. /**
  78. * @covers ParserOutput::getWrapperDivClass
  79. * @covers ParserOutput::addWrapperDivClass
  80. * @covers ParserOutput::clearWrapperDivClass
  81. * @covers ParserOutput::getText
  82. */
  83. public function testWrapperDivClass() {
  84. $po = new ParserOutput();
  85. $po->setText( 'Kittens' );
  86. $this->assertContains( 'Kittens', $po->getText() );
  87. $this->assertNotContains( '<div', $po->getText() );
  88. $this->assertSame( 'Kittens', $po->getRawText() );
  89. $po->addWrapperDivClass( 'foo' );
  90. $text = $po->getText();
  91. $this->assertContains( 'Kittens', $text );
  92. $this->assertContains( '<div', $text );
  93. $this->assertContains( 'class="foo"', $text );
  94. $po->addWrapperDivClass( 'bar' );
  95. $text = $po->getText();
  96. $this->assertContains( 'Kittens', $text );
  97. $this->assertContains( '<div', $text );
  98. $this->assertContains( 'class="foo bar"', $text );
  99. $po->addWrapperDivClass( 'bar' ); // second time does nothing, no "foo bar bar".
  100. $text = $po->getText( [ 'unwrap' => true ] );
  101. $this->assertContains( 'Kittens', $text );
  102. $this->assertNotContains( '<div', $text );
  103. $this->assertNotContains( 'class="foo bar"', $text );
  104. $text = $po->getText( [ 'wrapperDivClass' => '' ] );
  105. $this->assertContains( 'Kittens', $text );
  106. $this->assertNotContains( '<div', $text );
  107. $this->assertNotContains( 'class="foo bar"', $text );
  108. $text = $po->getText( [ 'wrapperDivClass' => 'xyzzy' ] );
  109. $this->assertContains( 'Kittens', $text );
  110. $this->assertContains( '<div', $text );
  111. $this->assertContains( 'class="xyzzy"', $text );
  112. $this->assertNotContains( 'class="foo bar"', $text );
  113. $text = $po->getRawText();
  114. $this->assertSame( 'Kittens', $text );
  115. $po->clearWrapperDivClass();
  116. $text = $po->getText();
  117. $this->assertContains( 'Kittens', $text );
  118. $this->assertNotContains( '<div', $text );
  119. $this->assertNotContains( 'class="foo bar"', $text );
  120. }
  121. public function testT203716() {
  122. // simulate extra wrapping from old parser cache
  123. $out = new ParserOutput( '<div class="mw-parser-output">Foo</div>' );
  124. $out = unserialize( serialize( $out ) );
  125. $plainText = $out->getText( [ 'unwrap' => true ] );
  126. $wrappedText = $out->getText( [ 'unwrap' => false ] );
  127. $wrappedText2 = $out->getText( [ 'wrapperDivClass' => 'mw-parser-output' ] );
  128. $this->assertNotContains( '<div', $plainText );
  129. $this->assertContains( '<div', $wrappedText );
  130. $this->assertStringNotMatchesFormat( '<div%s<div%s', $wrappedText );
  131. $this->assertContains( '<div', $wrappedText2 );
  132. $this->assertStringNotMatchesFormat( '<div%s<div%s', $wrappedText2 );
  133. // simulate ParserOuput creation by new parser code
  134. $out = new ParserOutput( 'Foo' );
  135. $out->addWrapperDivClass( 'mw-parser-outout' );
  136. $out = unserialize( serialize( $out ) );
  137. $plainText = $out->getText( [ 'unwrap' => true ] );
  138. $wrappedText = $out->getText( [ 'unwrap' => false ] );
  139. $wrappedText2 = $out->getText( [ 'wrapperDivClass' => 'mw-parser-output' ] );
  140. $this->assertNotContains( '<div', $plainText );
  141. $this->assertContains( '<div', $wrappedText );
  142. $this->assertStringNotMatchesFormat( '<div%s<div%s', $wrappedText );
  143. $this->assertContains( '<div', $wrappedText2 );
  144. $this->assertStringNotMatchesFormat( '<div%s<div%s', $wrappedText2 );
  145. }
  146. /**
  147. * @covers ParserOutput::getText
  148. * @dataProvider provideGetText
  149. * @param array $options Options to getText()
  150. * @param string $text Parser text
  151. * @param string $expect Expected output
  152. */
  153. public function testGetText( $options, $text, $expect ) {
  154. $this->setMwGlobals( [
  155. 'wgArticlePath' => '/wiki/$1',
  156. 'wgScriptPath' => '/w',
  157. 'wgScript' => '/w/index.php',
  158. ] );
  159. $po = new ParserOutput( $text );
  160. $actual = $po->getText( $options );
  161. $this->assertSame( $expect, $actual );
  162. }
  163. public static function provideGetText() {
  164. // phpcs:disable Generic.Files.LineLength
  165. $text = <<<EOF
  166. <p>Test document.
  167. </p>
  168. <mw:toc><div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div>
  169. <ul>
  170. <li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li>
  171. <li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a>
  172. <ul>
  173. <li class="toclevel-2 tocsection-3"><a href="#Section_2.1"><span class="tocnumber">2.1</span> <span class="toctext">Section 2.1</span></a></li>
  174. </ul>
  175. </li>
  176. <li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li>
  177. </ul>
  178. </div>
  179. </mw:toc>
  180. <h2><span class="mw-headline" id="Section_1">Section 1</span><mw:editsection page="Test Page" section="1">Section 1</mw:editsection></h2>
  181. <p>One
  182. </p>
  183. <h2><span class="mw-headline" id="Section_2">Section 2</span><mw:editsection page="Test Page" section="2">Section 2</mw:editsection></h2>
  184. <p>Two
  185. </p>
  186. <h3><span class="mw-headline" id="Section_2.1">Section 2.1</span><mw:editsection page="Test Page" section="3">Section 2.1</mw:editsection></h3>
  187. <p>Two point one
  188. </p>
  189. <h2><span class="mw-headline" id="Section_3">Section 3</span><mw:editsection page="Test Page" section="4">Section 3</mw:editsection></h2>
  190. <p>Three
  191. </p>
  192. EOF;
  193. $dedupText = <<<EOF
  194. <p>This is a test document.</p>
  195. <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
  196. <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
  197. <style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
  198. <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
  199. <style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
  200. <style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style>
  201. <style data-mw-deduplicate="duplicate1">.Same-attribute-different-content {}</style>
  202. <style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style>
  203. <style>.Duplicate1 {}</style>
  204. EOF;
  205. return [
  206. 'No options' => [
  207. [], $text, <<<EOF
  208. <p>Test document.
  209. </p>
  210. <div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div>
  211. <ul>
  212. <li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li>
  213. <li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a>
  214. <ul>
  215. <li class="toclevel-2 tocsection-3"><a href="#Section_2.1"><span class="tocnumber">2.1</span> <span class="toctext">Section 2.1</span></a></li>
  216. </ul>
  217. </li>
  218. <li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li>
  219. </ul>
  220. </div>
  221. <h2><span class="mw-headline" id="Section_1">Section 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
  222. <p>One
  223. </p>
  224. <h2><span class="mw-headline" id="Section_2">Section 2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=2" title="Edit section: Section 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
  225. <p>Two
  226. </p>
  227. <h3><span class="mw-headline" id="Section_2.1">Section 2.1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=3" title="Edit section: Section 2.1">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
  228. <p>Two point one
  229. </p>
  230. <h2><span class="mw-headline" id="Section_3">Section 3</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=4" title="Edit section: Section 3">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
  231. <p>Three
  232. </p>
  233. EOF
  234. ],
  235. 'Disable section edit links' => [
  236. [ 'enableSectionEditLinks' => false ], $text, <<<EOF
  237. <p>Test document.
  238. </p>
  239. <div id="toc" class="toc"><div class="toctitle"><h2>Contents</h2></div>
  240. <ul>
  241. <li class="toclevel-1 tocsection-1"><a href="#Section_1"><span class="tocnumber">1</span> <span class="toctext">Section 1</span></a></li>
  242. <li class="toclevel-1 tocsection-2"><a href="#Section_2"><span class="tocnumber">2</span> <span class="toctext">Section 2</span></a>
  243. <ul>
  244. <li class="toclevel-2 tocsection-3"><a href="#Section_2.1"><span class="tocnumber">2.1</span> <span class="toctext">Section 2.1</span></a></li>
  245. </ul>
  246. </li>
  247. <li class="toclevel-1 tocsection-4"><a href="#Section_3"><span class="tocnumber">3</span> <span class="toctext">Section 3</span></a></li>
  248. </ul>
  249. </div>
  250. <h2><span class="mw-headline" id="Section_1">Section 1</span></h2>
  251. <p>One
  252. </p>
  253. <h2><span class="mw-headline" id="Section_2">Section 2</span></h2>
  254. <p>Two
  255. </p>
  256. <h3><span class="mw-headline" id="Section_2.1">Section 2.1</span></h3>
  257. <p>Two point one
  258. </p>
  259. <h2><span class="mw-headline" id="Section_3">Section 3</span></h2>
  260. <p>Three
  261. </p>
  262. EOF
  263. ],
  264. 'Disable TOC, but wrap' => [
  265. [ 'allowTOC' => false, 'wrapperDivClass' => 'mw-parser-output' ], $text, <<<EOF
  266. <div class="mw-parser-output"><p>Test document.
  267. </p>
  268. <h2><span class="mw-headline" id="Section_1">Section 1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=1" title="Edit section: Section 1">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
  269. <p>One
  270. </p>
  271. <h2><span class="mw-headline" id="Section_2">Section 2</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=2" title="Edit section: Section 2">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
  272. <p>Two
  273. </p>
  274. <h3><span class="mw-headline" id="Section_2.1">Section 2.1</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=3" title="Edit section: Section 2.1">edit</a><span class="mw-editsection-bracket">]</span></span></h3>
  275. <p>Two point one
  276. </p>
  277. <h2><span class="mw-headline" id="Section_3">Section 3</span><span class="mw-editsection"><span class="mw-editsection-bracket">[</span><a href="/w/index.php?title=Test_Page&amp;action=edit&amp;section=4" title="Edit section: Section 3">edit</a><span class="mw-editsection-bracket">]</span></span></h2>
  278. <p>Three
  279. </p></div>
  280. EOF
  281. ],
  282. 'Style deduplication' => [
  283. [], $dedupText, <<<EOF
  284. <p>This is a test document.</p>
  285. <style data-mw-deduplicate="duplicate1">.Duplicate1 {}</style>
  286. <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1"/>
  287. <style data-mw-deduplicate="duplicate2">.Duplicate2 {}</style>
  288. <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1"/>
  289. <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate2"/>
  290. <style data-mw-not-deduplicate="duplicate1">.Duplicate1 {}</style>
  291. <link rel="mw-deduplicated-inline-style" href="mw-data:duplicate1"/>
  292. <style data-mw-deduplicate="duplicate3">.Duplicate1 {}</style>
  293. <style>.Duplicate1 {}</style>
  294. EOF
  295. ],
  296. 'Style deduplication disabled' => [
  297. [ 'deduplicateStyles' => false ], $dedupText, $dedupText
  298. ],
  299. ];
  300. // phpcs:enable
  301. }
  302. /**
  303. * @covers ParserOutput::hasText
  304. */
  305. public function testHasText() {
  306. $po = new ParserOutput();
  307. $this->assertTrue( $po->hasText() );
  308. $po = new ParserOutput( null );
  309. $this->assertFalse( $po->hasText() );
  310. $po = new ParserOutput( '' );
  311. $this->assertTrue( $po->hasText() );
  312. $po = new ParserOutput( null );
  313. $po->setText( '' );
  314. $this->assertTrue( $po->hasText() );
  315. }
  316. /**
  317. * @covers ParserOutput::getText
  318. */
  319. public function testGetText_failsIfNoText() {
  320. $po = new ParserOutput( null );
  321. $this->setExpectedException( LogicException::class );
  322. $po->getText();
  323. }
  324. /**
  325. * @covers ParserOutput::getRawText
  326. */
  327. public function testGetRawText_failsIfNoText() {
  328. $po = new ParserOutput( null );
  329. $this->setExpectedException( LogicException::class );
  330. $po->getRawText();
  331. }
  332. public function provideMergeHtmlMetaDataFrom() {
  333. // title text ------------
  334. $a = new ParserOutput();
  335. $a->setTitleText( 'X' );
  336. $b = new ParserOutput();
  337. yield 'only left title text' => [ $a, $b, [ 'getTitleText' => 'X' ] ];
  338. $a = new ParserOutput();
  339. $b = new ParserOutput();
  340. $b->setTitleText( 'Y' );
  341. yield 'only right title text' => [ $a, $b, [ 'getTitleText' => 'Y' ] ];
  342. $a = new ParserOutput();
  343. $a->setTitleText( 'X' );
  344. $b = new ParserOutput();
  345. $b->setTitleText( 'Y' );
  346. yield 'left title text wins' => [ $a, $b, [ 'getTitleText' => 'X' ] ];
  347. // index policy ------------
  348. $a = new ParserOutput();
  349. $a->setIndexPolicy( 'index' );
  350. $b = new ParserOutput();
  351. yield 'only left index policy' => [ $a, $b, [ 'getIndexPolicy' => 'index' ] ];
  352. $a = new ParserOutput();
  353. $b = new ParserOutput();
  354. $b->setIndexPolicy( 'index' );
  355. yield 'only right index policy' => [ $a, $b, [ 'getIndexPolicy' => 'index' ] ];
  356. $a = new ParserOutput();
  357. $a->setIndexPolicy( 'noindex' );
  358. $b = new ParserOutput();
  359. $b->setIndexPolicy( 'index' );
  360. yield 'left noindex wins' => [ $a, $b, [ 'getIndexPolicy' => 'noindex' ] ];
  361. $a = new ParserOutput();
  362. $a->setIndexPolicy( 'index' );
  363. $b = new ParserOutput();
  364. $b->setIndexPolicy( 'noindex' );
  365. yield 'right noindex wins' => [ $a, $b, [ 'getIndexPolicy' => 'noindex' ] ];
  366. // head items and friends ------------
  367. $a = new ParserOutput();
  368. $a->addHeadItem( '<foo1>' );
  369. $a->addHeadItem( '<bar1>', 'bar' );
  370. $a->addModules( 'test-module-a' );
  371. $a->addModuleScripts( 'test-module-script-a' );
  372. $a->addModuleStyles( 'test-module-styles-a' );
  373. $b->addJsConfigVars( 'test-config-var-a', 'a' );
  374. $b = new ParserOutput();
  375. $b->setIndexPolicy( 'noindex' );
  376. $b->addHeadItem( '<foo2>' );
  377. $b->addHeadItem( '<bar2>', 'bar' );
  378. $b->addModules( 'test-module-b' );
  379. $b->addModuleScripts( 'test-module-script-b' );
  380. $b->addModuleStyles( 'test-module-styles-b' );
  381. $b->addJsConfigVars( 'test-config-var-b', 'b' );
  382. $b->addJsConfigVars( 'test-config-var-a', 'X' );
  383. yield 'head items and friends' => [ $a, $b, [
  384. 'getHeadItems' => [
  385. '<foo1>',
  386. '<foo2>',
  387. 'bar' => '<bar2>', // overwritten
  388. ],
  389. 'getModules' => [
  390. 'test-module-a',
  391. 'test-module-b',
  392. ],
  393. 'getModuleScripts' => [
  394. 'test-module-script-a',
  395. 'test-module-script-b',
  396. ],
  397. 'getModuleStyles' => [
  398. 'test-module-styles-a',
  399. 'test-module-styles-b',
  400. ],
  401. 'getJsConfigVars' => [
  402. 'test-config-var-a' => 'X', // overwritten
  403. 'test-config-var-b' => 'b',
  404. ],
  405. ] ];
  406. // TOC ------------
  407. $a = new ParserOutput();
  408. $a->setTOCHTML( '<p>TOC A</p>' );
  409. $a->setSections( [ [ 'fromtitle' => 'A1' ], [ 'fromtitle' => 'A2' ] ] );
  410. $b = new ParserOutput();
  411. $b->setTOCHTML( '<p>TOC B</p>' );
  412. $b->setSections( [ [ 'fromtitle' => 'B1' ], [ 'fromtitle' => 'B2' ] ] );
  413. yield 'concat TOC' => [ $a, $b, [
  414. 'getTOCHTML' => '<p>TOC A</p><p>TOC B</p>',
  415. 'getSections' => [
  416. [ 'fromtitle' => 'A1' ],
  417. [ 'fromtitle' => 'A2' ],
  418. [ 'fromtitle' => 'B1' ],
  419. [ 'fromtitle' => 'B2' ]
  420. ],
  421. ] ];
  422. // Skin Control ------------
  423. $a = new ParserOutput();
  424. $a->setNewSection( true );
  425. $a->hideNewSection( true );
  426. $a->setNoGallery( true );
  427. $a->addWrapperDivClass( 'foo' );
  428. $a->setIndicator( 'foo', 'Foo!' );
  429. $a->setIndicator( 'bar', 'Bar!' );
  430. $a->setExtensionData( 'foo', 'Foo!' );
  431. $a->setExtensionData( 'bar', 'Bar!' );
  432. $b = new ParserOutput();
  433. $b->setNoGallery( true );
  434. $b->setEnableOOUI( true );
  435. $b->preventClickjacking( true );
  436. $a->addWrapperDivClass( 'bar' );
  437. $b->setIndicator( 'zoo', 'Zoo!' );
  438. $b->setIndicator( 'bar', 'Barrr!' );
  439. $b->setExtensionData( 'zoo', 'Zoo!' );
  440. $b->setExtensionData( 'bar', 'Barrr!' );
  441. yield 'skin control flags' => [ $a, $b, [
  442. 'getNewSection' => true,
  443. 'getHideNewSection' => true,
  444. 'getNoGallery' => true,
  445. 'getEnableOOUI' => true,
  446. 'preventClickjacking' => true,
  447. 'getIndicators' => [
  448. 'foo' => 'Foo!',
  449. 'bar' => 'Barrr!',
  450. 'zoo' => 'Zoo!',
  451. ],
  452. 'getWrapperDivClass' => 'foo bar',
  453. '$mExtensionData' => [
  454. 'foo' => 'Foo!',
  455. 'bar' => 'Barrr!',
  456. 'zoo' => 'Zoo!',
  457. ],
  458. ] ];
  459. }
  460. /**
  461. * @dataProvider provideMergeHtmlMetaDataFrom
  462. * @covers ParserOutput::mergeHtmlMetaDataFrom
  463. *
  464. * @param ParserOutput $a
  465. * @param ParserOutput $b
  466. * @param array $expected
  467. */
  468. public function testMergeHtmlMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
  469. $a->mergeHtmlMetaDataFrom( $b );
  470. $this->assertFieldValues( $a, $expected );
  471. // test twice, to make sure the operation is idempotent (except for the TOC, see below)
  472. $a->mergeHtmlMetaDataFrom( $b );
  473. // XXX: TOC joining should get smarter. Can we make it idempotent as well?
  474. unset( $expected['getTOCHTML'] );
  475. unset( $expected['getSections'] );
  476. $this->assertFieldValues( $a, $expected );
  477. }
  478. private function assertFieldValues( ParserOutput $po, $expected ) {
  479. $po = TestingAccessWrapper::newFromObject( $po );
  480. foreach ( $expected as $method => $value ) {
  481. if ( $method[0] === '$' ) {
  482. $field = substr( $method, 1 );
  483. $actual = $po->__get( $field );
  484. } else {
  485. $actual = $po->__call( $method, [] );
  486. }
  487. $this->assertEquals( $value, $actual, $method );
  488. }
  489. }
  490. public function provideMergeTrackingMetaDataFrom() {
  491. // links ------------
  492. $a = new ParserOutput();
  493. $a->addLink( Title::makeTitle( NS_MAIN, 'Kittens' ), 6 );
  494. $a->addLink( Title::makeTitle( NS_TALK, 'Kittens' ), 16 );
  495. $a->addLink( Title::makeTitle( NS_MAIN, 'Goats' ), 7 );
  496. $a->addTemplate( Title::makeTitle( NS_TEMPLATE, 'Goats' ), 107, 1107 );
  497. $a->addLanguageLink( 'de' );
  498. $a->addLanguageLink( 'ru' );
  499. $a->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Kittens DE', '', 'de' ) );
  500. $a->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Kittens RU', '', 'ru' ) );
  501. $a->addExternalLink( 'https://kittens.wikimedia.test' );
  502. $a->addExternalLink( 'https://goats.wikimedia.test' );
  503. $a->addCategory( 'Foo', 'X' );
  504. $a->addImage( 'Billy.jpg', '20180101000013', 'DEAD' );
  505. $b = new ParserOutput();
  506. $b->addLink( Title::makeTitle( NS_MAIN, 'Goats' ), 7 );
  507. $b->addLink( Title::makeTitle( NS_TALK, 'Goats' ), 17 );
  508. $b->addLink( Title::makeTitle( NS_MAIN, 'Dragons' ), 8 );
  509. $b->addLink( Title::makeTitle( NS_FILE, 'Dragons.jpg' ), 28 );
  510. $b->addTemplate( Title::makeTitle( NS_TEMPLATE, 'Dragons' ), 108, 1108 );
  511. $a->addTemplate( Title::makeTitle( NS_MAIN, 'Dragons' ), 118, 1118 );
  512. $b->addLanguageLink( 'fr' );
  513. $b->addLanguageLink( 'ru' );
  514. $b->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Kittens FR', '', 'fr' ) );
  515. $b->addInterwikiLink( Title::makeTitle( NS_MAIN, 'Dragons RU', '', 'ru' ) );
  516. $b->addExternalLink( 'https://dragons.wikimedia.test' );
  517. $b->addExternalLink( 'https://goats.wikimedia.test' );
  518. $b->addCategory( 'Bar', 'Y' );
  519. $b->addImage( 'Puff.jpg', '20180101000017', 'BEEF' );
  520. yield 'all kinds of links' => [ $a, $b, [
  521. 'getLinks' => [
  522. NS_MAIN => [
  523. 'Kittens' => 6,
  524. 'Goats' => 7,
  525. 'Dragons' => 8,
  526. ],
  527. NS_TALK => [
  528. 'Kittens' => 16,
  529. 'Goats' => 17,
  530. ],
  531. NS_FILE => [
  532. 'Dragons.jpg' => 28,
  533. ],
  534. ],
  535. 'getTemplates' => [
  536. NS_MAIN => [
  537. 'Dragons' => 118,
  538. ],
  539. NS_TEMPLATE => [
  540. 'Dragons' => 108,
  541. 'Goats' => 107,
  542. ],
  543. ],
  544. 'getTemplateIds' => [
  545. NS_MAIN => [
  546. 'Dragons' => 1118,
  547. ],
  548. NS_TEMPLATE => [
  549. 'Dragons' => 1108,
  550. 'Goats' => 1107,
  551. ],
  552. ],
  553. 'getLanguageLinks' => [ 'de', 'ru', 'fr' ],
  554. 'getInterwikiLinks' => [
  555. 'de' => [ 'Kittens_DE' => 1 ],
  556. 'ru' => [ 'Kittens_RU' => 1, 'Dragons_RU' => 1, ],
  557. 'fr' => [ 'Kittens_FR' => 1 ],
  558. ],
  559. 'getCategories' => [ 'Foo' => 'X', 'Bar' => 'Y' ],
  560. 'getImages' => [ 'Billy.jpg' => 1, 'Puff.jpg' => 1 ],
  561. 'getFileSearchOptions' => [
  562. 'Billy.jpg' => [ 'time' => '20180101000013', 'sha1' => 'DEAD' ],
  563. 'Puff.jpg' => [ 'time' => '20180101000017', 'sha1' => 'BEEF' ],
  564. ],
  565. 'getExternalLinks' => [
  566. 'https://dragons.wikimedia.test' => 1,
  567. 'https://kittens.wikimedia.test' => 1,
  568. 'https://goats.wikimedia.test' => 1,
  569. ]
  570. ] ];
  571. // properties ------------
  572. $a = new ParserOutput();
  573. $a->setProperty( 'foo', 'Foo!' );
  574. $a->setProperty( 'bar', 'Bar!' );
  575. $a->setExtensionData( 'foo', 'Foo!' );
  576. $a->setExtensionData( 'bar', 'Bar!' );
  577. $b = new ParserOutput();
  578. $b->setProperty( 'zoo', 'Zoo!' );
  579. $b->setProperty( 'bar', 'Barrr!' );
  580. $b->setExtensionData( 'zoo', 'Zoo!' );
  581. $b->setExtensionData( 'bar', 'Barrr!' );
  582. yield 'properties' => [ $a, $b, [
  583. 'getProperties' => [
  584. 'foo' => 'Foo!',
  585. 'bar' => 'Barrr!',
  586. 'zoo' => 'Zoo!',
  587. ],
  588. '$mExtensionData' => [
  589. 'foo' => 'Foo!',
  590. 'bar' => 'Barrr!',
  591. 'zoo' => 'Zoo!',
  592. ],
  593. ] ];
  594. }
  595. /**
  596. * @dataProvider provideMergeTrackingMetaDataFrom
  597. * @covers ParserOutput::mergeTrackingMetaDataFrom
  598. *
  599. * @param ParserOutput $a
  600. * @param ParserOutput $b
  601. * @param array $expected
  602. */
  603. public function testMergeTrackingMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
  604. $a->mergeTrackingMetaDataFrom( $b );
  605. $this->assertFieldValues( $a, $expected );
  606. // test twice, to make sure the operation is idempotent
  607. $a->mergeTrackingMetaDataFrom( $b );
  608. $this->assertFieldValues( $a, $expected );
  609. }
  610. public function provideMergeInternalMetaDataFrom() {
  611. // hooks
  612. $a = new ParserOutput();
  613. $a->addOutputHook( 'foo', 'X' );
  614. $a->addOutputHook( 'bar' );
  615. $b = new ParserOutput();
  616. $b->addOutputHook( 'foo', 'Y' );
  617. $b->addOutputHook( 'bar' );
  618. $b->addOutputHook( 'zoo' );
  619. yield 'hooks' => [ $a, $b, [
  620. 'getOutputHooks' => [
  621. [ 'foo', 'X' ],
  622. [ 'bar', false ],
  623. [ 'foo', 'Y' ],
  624. [ 'zoo', false ],
  625. ],
  626. ] ];
  627. // flags & co
  628. $a = new ParserOutput();
  629. $a->addWarning( 'Oops' );
  630. $a->addWarning( 'Whoops' );
  631. $a->setFlag( 'foo' );
  632. $a->setFlag( 'bar' );
  633. $a->recordOption( 'Foo' );
  634. $a->recordOption( 'Bar' );
  635. $b = new ParserOutput();
  636. $b->addWarning( 'Yikes' );
  637. $b->addWarning( 'Whoops' );
  638. $b->setFlag( 'zoo' );
  639. $b->setFlag( 'bar' );
  640. $b->recordOption( 'Zoo' );
  641. $b->recordOption( 'Bar' );
  642. yield 'flags' => [ $a, $b, [
  643. 'getWarnings' => [ 'Oops', 'Whoops', 'Yikes' ],
  644. '$mFlags' => [ 'foo' => true, 'bar' => true, 'zoo' => true ],
  645. 'getUsedOptions' => [ 'Foo', 'Bar', 'Zoo' ],
  646. ] ];
  647. // timestamp ------------
  648. $a = new ParserOutput();
  649. $a->setTimestamp( '20180101000011' );
  650. $b = new ParserOutput();
  651. yield 'only left timestamp' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
  652. $a = new ParserOutput();
  653. $b = new ParserOutput();
  654. $b->setTimestamp( '20180101000011' );
  655. yield 'only right timestamp' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
  656. $a = new ParserOutput();
  657. $a->setTimestamp( '20180101000011' );
  658. $b = new ParserOutput();
  659. $b->setTimestamp( '20180101000001' );
  660. yield 'left timestamp wins' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
  661. $a = new ParserOutput();
  662. $a->setTimestamp( '20180101000001' );
  663. $b = new ParserOutput();
  664. $b->setTimestamp( '20180101000011' );
  665. yield 'right timestamp wins' => [ $a, $b, [ 'getTimestamp' => '20180101000011' ] ];
  666. // speculative rev id ------------
  667. $a = new ParserOutput();
  668. $a->setSpeculativeRevIdUsed( 9 );
  669. $b = new ParserOutput();
  670. yield 'only left speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
  671. $a = new ParserOutput();
  672. $b = new ParserOutput();
  673. $b->setSpeculativeRevIdUsed( 9 );
  674. yield 'only right speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
  675. $a = new ParserOutput();
  676. $a->setSpeculativeRevIdUsed( 9 );
  677. $b = new ParserOutput();
  678. $b->setSpeculativeRevIdUsed( 9 );
  679. yield 'same speculative rev id' => [ $a, $b, [ 'getSpeculativeRevIdUsed' => 9 ] ];
  680. // limit report (recursive max) ------------
  681. $a = new ParserOutput();
  682. $a->setLimitReportData( 'naive1', 7 );
  683. $a->setLimitReportData( 'naive2', 27 );
  684. $a->setLimitReportData( 'limitreport-simple1', 7 );
  685. $a->setLimitReportData( 'limitreport-simple2', 27 );
  686. $a->setLimitReportData( 'limitreport-pair1', [ 7, 9 ] );
  687. $a->setLimitReportData( 'limitreport-pair2', [ 27, 29 ] );
  688. $a->setLimitReportData( 'limitreport-more1', [ 7, 9, 1 ] );
  689. $a->setLimitReportData( 'limitreport-more2', [ 27, 29, 21 ] );
  690. $a->setLimitReportData( 'limitreport-only-a', 13 );
  691. $b = new ParserOutput();
  692. $b->setLimitReportData( 'naive1', 17 );
  693. $b->setLimitReportData( 'naive2', 17 );
  694. $b->setLimitReportData( 'limitreport-simple1', 17 );
  695. $b->setLimitReportData( 'limitreport-simple2', 17 );
  696. $b->setLimitReportData( 'limitreport-pair1', [ 17, 19 ] );
  697. $b->setLimitReportData( 'limitreport-pair2', [ 17, 19 ] );
  698. $b->setLimitReportData( 'limitreport-more1', [ 17, 19, 11 ] );
  699. $b->setLimitReportData( 'limitreport-more2', [ 17, 19, 11 ] );
  700. $b->setLimitReportData( 'limitreport-only-b', 23 );
  701. // first write wins
  702. yield 'limit report' => [ $a, $b, [
  703. 'getLimitReportData' => [
  704. 'naive1' => 7,
  705. 'naive2' => 27,
  706. 'limitreport-simple1' => 7,
  707. 'limitreport-simple2' => 27,
  708. 'limitreport-pair1' => [ 7, 9 ],
  709. 'limitreport-pair2' => [ 27, 29 ],
  710. 'limitreport-more1' => [ 7, 9, 1 ],
  711. 'limitreport-more2' => [ 27, 29, 21 ],
  712. 'limitreport-only-a' => 13,
  713. ],
  714. 'getLimitReportJSData' => [
  715. 'naive1' => 7,
  716. 'naive2' => 27,
  717. 'limitreport' => [
  718. 'simple1' => 7,
  719. 'simple2' => 27,
  720. 'pair1' => [ 'value' => 7, 'limit' => 9 ],
  721. 'pair2' => [ 'value' => 27, 'limit' => 29 ],
  722. 'more1' => [ 7, 9, 1 ],
  723. 'more2' => [ 27, 29, 21 ],
  724. 'only-a' => 13,
  725. ],
  726. ],
  727. ] ];
  728. }
  729. /**
  730. * @dataProvider provideMergeInternalMetaDataFrom
  731. * @covers ParserOutput::mergeInternalMetaDataFrom
  732. *
  733. * @param ParserOutput $a
  734. * @param ParserOutput $b
  735. * @param array $expected
  736. */
  737. public function testMergeInternalMetaDataFrom( ParserOutput $a, ParserOutput $b, $expected ) {
  738. $a->mergeInternalMetaDataFrom( $b );
  739. $this->assertFieldValues( $a, $expected );
  740. // test twice, to make sure the operation is idempotent
  741. $a->mergeInternalMetaDataFrom( $b );
  742. $this->assertFieldValues( $a, $expected );
  743. }
  744. public function testMergeInternalMetaDataFrom_parseStartTime() {
  745. /** @var object $a */
  746. $a = new ParserOutput();
  747. $a = TestingAccessWrapper::newFromObject( $a );
  748. $a->resetParseStartTime();
  749. $aClocks = $a->mParseStartTime;
  750. $b = new ParserOutput();
  751. $a->mergeInternalMetaDataFrom( $b );
  752. $mergedClocks = $a->mParseStartTime;
  753. foreach ( $mergedClocks as $clock => $timestamp ) {
  754. $this->assertSame( $aClocks[$clock], $timestamp, $clock );
  755. }
  756. // try again, with times in $b also set, and later than $a's
  757. usleep( 1234 );
  758. /** @var object $b */
  759. $b = new ParserOutput();
  760. $b = TestingAccessWrapper::newFromObject( $b );
  761. $b->resetParseStartTime();
  762. $bClocks = $b->mParseStartTime;
  763. $a->mergeInternalMetaDataFrom( $b->object, 'b' );
  764. $mergedClocks = $a->mParseStartTime;
  765. foreach ( $mergedClocks as $clock => $timestamp ) {
  766. $this->assertSame( $aClocks[$clock], $timestamp, $clock );
  767. $this->assertLessThanOrEqual( $bClocks[$clock], $timestamp, $clock );
  768. }
  769. // try again, with $a's times being later
  770. usleep( 1234 );
  771. $a->resetParseStartTime();
  772. $aClocks = $a->mParseStartTime;
  773. $a->mergeInternalMetaDataFrom( $b->object, 'b' );
  774. $mergedClocks = $a->mParseStartTime;
  775. foreach ( $mergedClocks as $clock => $timestamp ) {
  776. $this->assertSame( $bClocks[$clock], $timestamp, $clock );
  777. $this->assertLessThanOrEqual( $aClocks[$clock], $timestamp, $clock );
  778. }
  779. // try again, with no times in $a set
  780. $a = new ParserOutput();
  781. $a = TestingAccessWrapper::newFromObject( $a );
  782. $a->mergeInternalMetaDataFrom( $b->object, 'b' );
  783. $mergedClocks = $a->mParseStartTime;
  784. foreach ( $mergedClocks as $clock => $timestamp ) {
  785. $this->assertSame( $bClocks[$clock], $timestamp, $clock );
  786. }
  787. }
  788. }