diff --git a/docs/reference/extending.md b/docs/reference/extending.md index 218e38a74..32f691071 100644 --- a/docs/reference/extending.md +++ b/docs/reference/extending.md @@ -87,7 +87,16 @@ class StaticPagePropertyProvider implements PagePropertyProvider { ``` Register with `NeoWikiRegistrar::addPagePropertyProvider()`. The context exposes the page id, title, -creation and modification times, categories, and last editor. Example: +creation and modification times, categories, and last editor, so providers can derive Page Properties +from the page content without re-fetching or re-parsing it. + +To derive Page Properties from the content, prefer the parse products: `categories`, and +`parserProperties` — the MediaWiki page properties recorded during parsing (e.g. those a parser hook +sets via `ParserOutput::setPageProperty`). These are template-expansion-safe and robust. (Note that +`parserProperties` are an input from MediaWiki's parse; they are not the NeoWiki Page Properties this +provider returns.) The raw main slot `content` and its `contentModel` are also exposed, but scraping +raw wikitext is fragile — reach for them mainly when handling a custom, non-wikitext content model that +the parse products do not cover. Example: [`src/StaticPagePropertyProvider.php`](https://github.com/ProfessionalWiki/NeoWiki/blob/master/tests/RedHerb/src/StaticPagePropertyProvider.php). ### Reading NeoWiki data and authorization diff --git a/src/Domain/Page/PagePropertyProviderContext.php b/src/Domain/Page/PagePropertyProviderContext.php index bb14488d7..a2877b8bc 100644 --- a/src/Domain/Page/PagePropertyProviderContext.php +++ b/src/Domain/Page/PagePropertyProviderContext.php @@ -13,6 +13,14 @@ * @param string $modificationTime In the standard MediaWiki format, ie 20230726163439 * @param string[] $categories * @param string $lastEditor Plain username of the last editor, e.g. "JohnDoe". Empty string if unknown. + * @param string $content Serialized main slot content of the revision, e.g. the wikitext. + * Empty string if the content is unavailable. + * @param string $contentModel Content model of the main slot, e.g. "wikitext". + * Empty string if the content is unavailable. + * @param array $parserProperties Properties recorded during parsing of the + * content (MediaWiki page properties, e.g. "defaultsort" or values set by parser hooks via + * ParserOutput::setPageProperty). These are inputs from the parse, not to be confused with the NeoWiki + * Page Properties that providers return. */ public function __construct( public PageId $pageId, @@ -22,6 +30,9 @@ public function __construct( public string $modificationTime, public array $categories, public string $lastEditor, + public string $content, + public string $contentModel, + public array $parserProperties, ) { } diff --git a/src/PagePropertiesBuilder.php b/src/PagePropertiesBuilder.php index f0c2a27c1..b24f4dd26 100644 --- a/src/PagePropertiesBuilder.php +++ b/src/PagePropertiesBuilder.php @@ -4,8 +4,10 @@ namespace ProfessionalWiki\NeoWiki; +use MediaWiki\Content\Content; use MediaWiki\Content\IContentHandlerFactory; use MediaWiki\Content\Renderer\ContentParseParams; +use MediaWiki\Parser\ParserOutput; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Revision\RevisionStore; use MediaWiki\Revision\SlotRecord; @@ -40,6 +42,8 @@ public function getPagePropertiesFor( RevisionRecord $revision, ?UserIdentity $u private function buildContext( RevisionRecord $revision, ?UserIdentity $user ): PagePropertyProviderContext { $linkTarget = $revision->getPageAsLinkTarget(); + $content = $revision->getContent( SlotRecord::MAIN ); + $parserOutput = $content === null ? null : $this->parse( $content, $revision ); return new PagePropertyProviderContext( pageId: new PageId( $revision->getPageId() ), @@ -47,11 +51,19 @@ private function buildContext( RevisionRecord $revision, ?UserIdentity $user ): namespaceId: $linkTarget->getNamespace(), creationTime: $this->getCreationTime( $revision ), modificationTime: $this->getModificationTime( $revision ), - categories: $this->getCategories( $revision ), + categories: $parserOutput === null ? [] : $parserOutput->getCategoryNames(), lastEditor: $user?->getName() ?? '', + content: $content === null ? '' : $content->serialize(), + contentModel: $content === null ? '' : $content->getModel(), + parserProperties: $parserOutput === null ? [] : $parserOutput->getPageProperties(), ); } + private function parse( Content $content, RevisionRecord $revision ): ParserOutput { + return $this->contentHandlerFactory->getContentHandler( $content->getModel() ) + ->getParserOutput( $content, new ContentParseParams( $revision->getPage() ) ); + } + private function getCreationTime( RevisionRecord $revision ): string { $time = $this->revisionStore->getFirstRevision( $revision->getPage() )?->getTimestamp(); @@ -72,19 +84,4 @@ private function getModificationTime( RevisionRecord $revision ): string { return $time; } - /** - * @return string[] - */ - private function getCategories( RevisionRecord $revision ): array { - $content = $revision->getContent( SlotRecord::MAIN ); - - if ( $content === null ) { - return []; - } - - return $this->contentHandlerFactory->getContentHandler( $content->getModel() ) - ->getParserOutput( $content, new ContentParseParams( $revision->getPage() ) ) - ->getCategoryNames(); - } - } diff --git a/tests/phpunit/PagePropertiesBuilderTest.php b/tests/phpunit/PagePropertiesBuilderTest.php new file mode 100644 index 000000000..f1e18e332 --- /dev/null +++ b/tests/phpunit/PagePropertiesBuilderTest.php @@ -0,0 +1,66 @@ +getContextForNewPageWithContent( 'Some text [[Category:Cats]]' ); + + $this->assertSame( 'Some text [[Category:Cats]]', $context->content ); + } + + public function testProviderReceivesContentModel(): void { + $context = $this->getContextForNewPageWithContent( 'Whatever wikitext' ); + + $this->assertSame( CONTENT_MODEL_WIKITEXT, $context->contentModel ); + } + + public function testProviderReceivesParserRecordedProperties(): void { + $context = $this->getContextForNewPageWithContent( 'Sorted {{DEFAULTSORT:Zebra}}' ); + + $this->assertSame( 'Zebra', $context->parserProperties['defaultsort'] ); + } + + public function testProviderReceivesCategoriesFromParsedContent(): void { + $context = $this->getContextForNewPageWithContent( 'Some text [[Category:Cats]]' ); + + $this->assertSame( [ 'Cats' ], $context->categories ); + } + + private function getContextForNewPageWithContent( string $wikitext ): PagePropertyProviderContext { + $revision = $this->editPage( 'PagePropertiesBuilderTestPage', $wikitext )->getNewRevision(); + + $spy = new SpyPagePropertyProvider(); + + $this->newPagePropertiesBuilder( $spy )->getPagePropertiesFor( $revision, null ); + + return $spy->getReceivedContext(); + } + + private function newPagePropertiesBuilder( SpyPagePropertyProvider $provider ): PagePropertiesBuilder { + $registry = new PagePropertyProviderRegistry(); + $registry->addProvider( $provider ); + + $services = $this->getServiceContainer(); + + return new PagePropertiesBuilder( + revisionStore: $services->getRevisionStore(), + contentHandlerFactory: $services->getContentHandlerFactory(), + titleFormatter: $services->getTitleFormatter(), + providerRegistry: $registry, + ); + } + +} diff --git a/tests/phpunit/Persistence/CorePagePropertyProviderTest.php b/tests/phpunit/Persistence/CorePagePropertyProviderTest.php index 9b26a2116..4573ca393 100644 --- a/tests/phpunit/Persistence/CorePagePropertyProviderTest.php +++ b/tests/phpunit/Persistence/CorePagePropertyProviderTest.php @@ -82,6 +82,9 @@ private function getPropertiesForContext( modificationTime: $modificationTime, categories: $categories, lastEditor: $lastEditor, + content: 'Whatever content', + contentModel: 'wikitext', + parserProperties: [], ) ); } diff --git a/tests/phpunit/TestDoubles/SpyPagePropertyProvider.php b/tests/phpunit/TestDoubles/SpyPagePropertyProvider.php new file mode 100644 index 000000000..63951b573 --- /dev/null +++ b/tests/phpunit/TestDoubles/SpyPagePropertyProvider.php @@ -0,0 +1,27 @@ +receivedContext = $context; + return []; + } + + public function getReceivedContext(): PagePropertyProviderContext { + if ( $this->receivedContext === null ) { + throw new \LogicException( 'getProperties was not called' ); + } + + return $this->receivedContext; + } + +}