Samwilson has uploaded a new change for review. https://gerrit.wikimedia.org/r/321215
Change subject: Better tree drawing ...................................................................... Better tree drawing Change-Id: I9c5fad962b52e0dcb64038935ee84b15c7a1f6fb --- M Genealogy.i18n.php M README.md A person_template.wikitext M src/Person.php M src/Traverser.php M src/Tree.php M src/Util.php M tests/phpunit/PersonTest.php 8 files changed, 224 insertions(+), 43 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Genealogy refs/changes/15/321215/1 diff --git a/Genealogy.i18n.php b/Genealogy.i18n.php index db0617a..9a74409 100644 --- a/Genealogy.i18n.php +++ b/Genealogy.i18n.php @@ -13,12 +13,14 @@ * @author Sam Wilson <[email protected]> */ $messages['en'] = [ - 'genealogy' => "Genealogy", - 'genealogy-desc' => "Adds a parser function for easier linking between genealogical records", + 'genealogy' => 'Genealogy', + 'genealogy-desc' => 'Adds a parser function for easier linking between genealogical records', 'genealogy-born' => 'b.', 'genealogy-died' => 'd.', - 'genealogy-ancestor' => 'Ancestor', - 'genealogy-descendant' => 'Descendant', - 'genealogy-ancestors' => 'Ancestors', - 'genealogy-descendants' => 'Descendants', + 'genealogy-ancestor' => 'Ancestor', + 'genealogy-ancestors' => 'Ancestors', + 'genealogy-descendant' => 'Descendant', + 'genealogy-descendants' => 'Descendants', + 'genealogy-person-preload' => 'Template:Person/preload', + 'genealogy-parser-function-not-found' => 'Genealogy parser function type not recognised: $1', ]; diff --git a/README.md b/README.md index 0dda15b..49b4935 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ -# MediaWiki Genealogy extension +MediaWiki Genealogy extension +============================= -## Usage +All details: +[mediawiki.org/wiki/Extension:Genealogy](https://mediawiki.org/wiki/Extension:Genealogy) -There is only one parser function, `{{#genealogy:}}`. -Its first two parameters are unnamed (i.e. don't have equals signs), -but all others must be (dates, etc.). +## Usage summary + +This extension creates one parser function: `{{#genealogy: … }}`. +Its first two parameters are unnamed (i.e. don't have equals signs) +but all others are (the dates, etc.). The following functions are supported, three for defining data and four for reporting data: @@ -25,10 +29,24 @@ `{{#genealogy:tree|ancestors=List|descendants=List}}` where each `List` is a newline-separated list of page titles. +## Templates + +**Example:** +For an example template that makes use of these parser functions, +see [`person_template.wikitext`](https://github.com/samwilson/Genealogy) + +**Preload:** +When this extension creates a link to a page that doesn't yet exist, +the text of `[[Template:Person/preload]]` is preloaded. +The location of this preload text can be customised +by modifying the `[[MediaWiki:genealogy-person-preload]]` system message. + ## Development The *Genealogy* extension is developed by Sam Wilson and released under version 3 of the GPL (see `LICENSE.txt` for details). +You can see this extension in use on [ArchivesWiki](https://archives.org.au). + Please report all bugs via the GitHub issue tracker at https://github.com/samwilson/Genealogy/issues diff --git a/person_template.wikitext b/person_template.wikitext new file mode 100644 index 0000000..0717ecb --- /dev/null +++ b/person_template.wikitext @@ -0,0 +1,43 @@ +{|class=wikitable +|+ {{PAGENAME}} +|- +! Birth: +| {{{birth_date|}}} {{{birth_place|}}} +|- +! Death: +| {{{death_date|}}} {{{death_place|}}} +|- +! Parents: +| +{{#if: {{{parent1|}}} | * {{#genealogy:parent | {{{parent1}}} }} }} +{{#if: {{{parent2|}}} | * {{#genealogy:parent | {{{parent2}}} }} }} +{{#if: {{{parent3|}}} | * {{#genealogy:parent | {{{parent3}}} }} }} +|- +! Siblings: +| {{#genealogy:siblings}} +|- +! Partners: +| {{#genealogy:partners}} +{{#if: {{{partner1|}}} | {{#genealogy:partner | {{{partner1}}} }} }} +{{#if: {{{partner2|}}} | {{#genealogy:partner | {{{partner2}}} }} }} +{{#if: {{{partner3|}}} | {{#genealogy:partner | {{{partner3}}} }} }} +|- +! Children: +| {{#genealogy:children}} +|}[[Category:People]]<noinclude> +This template is used to define and display a summary table on a biography article. + +It adds articles to the [[:Category:People|People]] category. + +== Usage == + +<pre><nowiki> +{{person + | parent1 = Person 1 Name + | parent2 = Person 2 Name + | parent3 = Person 3 Name + | partner1 = Person 4 Name + | partner2 = Person 5 Name + | partner3 = Person 6 Name +}} +</nowiki></pre> diff --git a/src/Person.php b/src/Person.php index 905fe3f..b1b5242 100644 --- a/src/Person.php +++ b/src/Person.php @@ -2,7 +2,9 @@ namespace Samwilson\Genealogy; +use Linker; use MagicWord; +use MediaWiki\Linker\LinkRenderer; use Parser; use Title; use WikiPage; @@ -145,11 +147,9 @@ * @return Person[] An array of parents, possibly empty. */ public function getParents() { - if ( is_array( $this->parents ) ) { - return $this->parents; - } - $this->parents = $this->getPropMulti( 'parent' ); - return $this->parents; + $parents = $this->getPropMulti( 'parent' ); + //ksort( $parents, SORT_REGULAR ); + return $parents; } /** @@ -247,7 +247,8 @@ 'pp_page' => $articleIds, "pp_propname LIKE 'genealogy $type %'" ], - __METHOD__ + __METHOD__, + [ 'ORDER BY' => 'pp_value' ] ); foreach ( $results as $result ) { $title = Title::newFromText( $result->pp_value ); diff --git a/src/Traverser.php b/src/Traverser.php index 9f393b6..7ce38ef 100644 --- a/src/Traverser.php +++ b/src/Traverser.php @@ -20,10 +20,16 @@ } public function ancestors( Person $person, $depth = null ) { + // Visit this person and their partners. $this->visit( $person ); + foreach ( $person->getPartners() as $partner ) { + $this->visit( $partner ); + } + // Give up if we're being limited. if ( $this->ancestor_depth > $depth ) { return; } + // Carry on to their ancestors. foreach ( $person->getParents() as $parent ) { $this->ancestors( $parent ); } @@ -31,10 +37,16 @@ } public function descendants( Person $person, $depth = null ) { + // Visit this person and their partners. $this->visit( $person ); + foreach ( $person->getPartners() as $partner ) { + $this->visit( $partner ); + } + // Give up if we're being limited. if ( $this->descendant_depth > $depth ) { return; } + // Carry on to their descendants. foreach ( $person->getChildren() as $parent ) { $this->descendants( $parent ); } diff --git a/src/Tree.php b/src/Tree.php index 38aea60..394151c 100644 --- a/src/Tree.php +++ b/src/Tree.php @@ -48,7 +48,7 @@ public function getGraphviz() { $this->out( 'top', 'digraph GenealogyTree {' ); - $this->out( 'top', 'graph [rankdir=LR, splines=ortho]' ); + $this->out( 'top', 'graph [rankdir=LR]' ); $this->out( 'top', 'edge [arrowhead=none]' ); $traverser = new Traverser(); @@ -68,10 +68,16 @@ } // Combine all parts of the graph output. - return join( "\n", $this->dot_source['top'] ) . "\n\n" - .join( "\n", $this->dot_source['person'] ) . "\n\n" - .join( "\n", $this->dot_source['partner'] ) . "\n\n" - .join( "\n", $this->dot_source['child'] ) . "\n}"; + $out = join( "\n", $this->dot_source['top'] ) . "\n\n" + . "node [ shape=plaintext ]\n" + . join( "\n", $this->dot_source['person'] ) . "\n\n"; + if (isset($this->dot_source['partner'])) { + $out .= join( "\n", $this->dot_source['partner'] ) . "\n\n"; + } + if (isset($this->dot_source['child'])) { + $out .= join( "\n", $this->dot_source['child'] ) . "\n\n"; + } + return $out . "}"; } public function visit( Person $person ) { @@ -86,20 +92,76 @@ } else { $date = ''; } - $personId = $this->esc( $person->getTitle()->getDBkey() ); - $url = $person->getTitle()->getFullURL(); + $personId = $this->esc( $person->getTitle()->getText() ); + $query = $person->getTitle()->exists() + ? [] + : [ 'preload' => wfMessage( 'genealogy-person-preload' ), 'action' => 'edit' ]; + $url = $person->getTitle()->getFullURL( $query ); $title = $person->getTitle(); - $line = $personId." [label=\"$title$date\", shape=plaintext, URL=\"$url\", " - . "tooltip=\"$title\"]"; + $line = $personId." [label=\"$title$date\", URL=\"$url\", " . "tooltip=\"$title\"]"; $this->out( 'person', $line ); - foreach ( $person->getChildren() as $child ) { - $parents = 'parents_'.$this->esc( join( '', $child->getParents() ) ); - $this->out( 'partner', $parents.' [label="", shape="point"]' ); - $this->out( 'partner', $personId.' -> '.$parents.' [style=dotted]' ); - $this->out( 'child', $parents.' -> '.$this->esc( $child->getTitle()->getDBkey() ) ); + + $partnerStyle = 'dashed'; + + // Output links to parents. + if ( $person->getParents() ) { + $parentsNode = $this->esc( join( '', $person->getParents() ) ); + $this->out( 'partner', $parentsNode . ' [label="", shape="point"]' ); + $this->out( 'child', $parentsNode . ' -> ' . $personId ); + foreach ( $person->getParents() as $parent ) { + $parentId = $this->esc( $parent->getTitle()->getText() ); + // If the parent doesn't exist, create its node now. + if ( !$parent->getTitle()->exists() ) { + $this->out( 'person', $parentId . ' [style=plaintext, fontcolor=red]' ); + } + $this->out( + 'partner', + $parentId . ' -> ' . $parentsNode . " [style=$partnerStyle]" + ); + } } + + // Output links to partners. + foreach ( $person->getPartners() as $partner ) { + // Create a point node for each partnership. + $partnerId = $this->esc( $partner->getTitle()->getDBkey() ); + $partners = [ $personId, $partnerId ]; + sort( $partners ); + $partnersNode = $this->esc( join( '', $partners ) ); + $this->out( 'partner', $partnersNode.' [label="", shape="point"]' ); + // Link this person and this partner to that point node. + $this->out( 'partner', $personId .' -> '. $partnersNode." [style=$partnerStyle, " + ." decorate=true]" ); + $this->out( 'partner', $partnerId .' -> '. $partnersNode." [style=$partnerStyle, " + ." decorate=true]" ); + // Create a node for a non-existing partner. + if ( !$partner->getTitle()->exists() ) { + $this->out( 'person', $partnerId . ' [shape=plaintext, fontcolor=red]' ); + } + } + + // Output links to children. + foreach ( $person->getChildren() as $child ) { + $parentsNode = $this->esc( join( '', $child->getParents() ) ); + $this->out( 'partner', $parentsNode.' [label="", shape="point"]' ); + $this->out( 'partner', $personId.' -> '.$parentsNode." [style=$partnerStyle]" ); + $childId = $this->esc( $child->getTitle()->getDBkey() ); + $this->out( 'child', $parentsNode.' -> ' . $childId ); + // Add this child if they don't exist. + if ( !$child->getTitle()->exists() ) { + $this->out( 'person', $childId . ' [shape=plaintext, fontcolor=red]' ); + } + } + } + /** + * Store a single line of Dot source output. This means we can avoid duplicate output lines, + * and also group source by different categories ('partner', 'child', etc.). + * @param string $group + * @param string $line The line of Dot source code. + * @param boolean $permit_dupes Add this line even if it's already there. + */ private function out( $group, $line, $permit_dupes = false ) { if ( !is_array( $this->dot_source ) ) { $this->dot_source = []; @@ -112,6 +174,13 @@ } } + /** + * Create a Dot-compatible variable name from any string (replace parentheses, spaces, and + * hyphens with underscores). + * @todo Complete the disallowed character list. + * @param string $title + * @return string + */ private function esc( $title ) { return strtr( $title, '( )-', '____' ); } diff --git a/src/Util.php b/src/Util.php index 787b657..0f7e49b 100644 --- a/src/Util.php +++ b/src/Util.php @@ -3,8 +3,11 @@ namespace Samwilson\Genealogy; use EditPage; +use Linker; +use MediaWiki\Linker\LinkRendererFactory; use Parser; use Title; +use Xml; class Util { @@ -78,6 +81,7 @@ } } $out = ''; // "<pre>".print_r($params, true)."</pre>"; + $isHtml = false; switch ( $type ) { case 'person': if ( isset( $params['birth date'] ) ) { @@ -91,29 +95,30 @@ break; case 'parent': $parentTitle = Title::newFromText( $params[0] ); - if ( $parentTitle and $parentTitle->exists() ) { + if ( $parentTitle && $parentTitle->exists() ) { $parent = new Person( $parentTitle ); $out .= $parent->getWikiLink(); } else { - $out .= "[[" . $params[0] . "]]"; + $query = [ 'preload' => wfMessage( 'genealogy-person-preload' ) ]; + $out .= Linker::link( $parentTitle, null, [], $query ); + $isHtml = true; } self::saveProp( $parser, 'parent', $params[0] ); break; case 'siblings': $person = new Person( $parser->getTitle() ); - $out .= self::PeopleList( $person->getSiblings() ); + $out .= self::peopleList( $person->getSiblings() ); break; case 'partner': - // $out .= "[[".$params[0]."]]"; self::saveProp( $parser, 'partner', $params[0] ); break; case 'partners': $person = new Person( $parser->getTitle() ); - $out .= self::PeopleList( $person->getPartners() ); + $out .= self::peopleList( $person->getPartners() ); break; case 'children': $person = new Person( $parser->getTitle() ); - $out .= self::PeopleList( $person->getChildren() ); + $out .= self::peopleList( $person->getChildren() ); break; case 'tree': $tree = new Tree(); @@ -127,15 +132,17 @@ // $tree->setDescendantDepth($params['descendant depth']); $graphviz = $tree->getGraphviz(); $out .= $parser->recursiveTagParse( "<graphviz>\n$graphviz\n</graphviz>" ); - $out .= "<pre>$graphviz</pre>"; + $out .= $parser->recursiveTagParse( + "<syntaxhighlight lines='1'>$graphviz</syntaxhighlight>" + ); break; default: - $out .= '<span class="error">' - . 'Genealogy parser function type not recognised: "' . $type . '".' - . '</span>'; + $msg = wfMessage('genealogy-parser-function-not-found', [ $type ] ); + $out .= "<span class='error'>$msg</span>"; break; } - return $out; + // Return format is documented in Parser::setFunctionHook(). + return $isHtml ? [ 0 => $out, 'isHTML' => true ] : $out; } /** @@ -170,7 +177,7 @@ * @param Person[] $people * @return string Wikitext list of people. */ - public static function PeopleList( $people ) { + public static function peopleList( $people ) { $out = ''; foreach ( $people as $person ) { $out .= "* " . $person->getWikiLink() . "\n"; diff --git a/tests/phpunit/PersonTest.php b/tests/phpunit/PersonTest.php index 5247e3e..d214556 100644 --- a/tests/phpunit/PersonTest.php +++ b/tests/phpunit/PersonTest.php @@ -7,6 +7,21 @@ */ class PersonTest extends MediaWikiTestCase { + /** + * Set the wikitext contents of a test page. + * @param string|Title $title The title of the page. + * @param string $wikitext The page contents. + * @return WikiPage + */ + protected function setPageContent( $title, $wikitext ) { + if ( is_string( $title ) ) { + $title = Title::newFromText( $title ); + } + $page = new WikiPage( $title ); + $page->doEditContent( new WikitextContent( $wikitext ), '' ); + return $page; + } + public function testCreatePerson() { $charlesTitle = Title::newFromText( 'Charles' ); $page = new WikiPage( $charlesTitle ); @@ -31,6 +46,20 @@ $this->assertEquals( '1890', $person->getDateYear( 'c. 1890' ) ); } + public function testParentsInAlphabeticalOrder() { + $alice = new Person( Title::newFromText( 'Alice' ) ); + $this->setPageContent( 'Alice', '{{#genealogy:parent|Clara}}{{#genealogy:parent|Bob}}' ); + $parents = $alice->getParents(); + $this->assertEquals( [ 'Bob', 'Clara' ], array_keys( $parents ) ); + } + + public function testPartnersInAlphabeticalOrder() { + $alice = new Person( Title::newFromText( 'Alice' ) ); + $this->setPageContent( 'Alice', '{{#genealogy:parent|Clara}}{{#genealogy:parent|Bob}}' ); + $parents = $alice->getParents(); + $this->assertEquals( [ 'Bob', 'Clara' ], array_keys( $parents ) ); + } + public function testRedirectPartner() { // Create Charles. $charlesTitle = Title::newFromText( 'Charles' ); -- To view, visit https://gerrit.wikimedia.org/r/321215 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I9c5fad962b52e0dcb64038935ee84b15c7a1f6fb Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/Genealogy Gerrit-Branch: master Gerrit-Owner: Samwilson <[email protected]> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
