Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4147032
ComputeSelectiveStats.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
7 KB
Referenced Files
None
Subscribers
None
ComputeSelectiveStats.php
View Options
<?php
declare
(
strict_types
=
1
);
namespace
Wikimedia\Parsoid\Utils
;
use
Wikimedia\Parsoid\Config\Env
;
use
Wikimedia\Parsoid\Config\PageConfig
;
use
Wikimedia\Parsoid\Core\PageBundle
;
use
Wikimedia\Parsoid\DOM\Document
;
use
Wikimedia\Parsoid\DOM\Element
;
use
Wikimedia\Parsoid\Html2Wt\DiffUtils
;
use
Wikimedia\Parsoid\Html2Wt\DOMDiff
;
use
Wikimedia\Parsoid\NodeData\TemplateInfo
;
/**
* This file contains code to classify opportunities for selective
* update and collect statistics.
*/
class
ComputeSelectiveStats
{
/** @return array<string,string> */
public
static
function
classify
(
Env
$env
,
?
PageConfig
$oldPage
,
?
PageBundle
$oldPb
,
PageConfig
$newPage
,
PageBundle
$newPb
):
array
{
// Default labels (ensure keys are consistent & in consistent order)
$labels
=
[
'type'
=>
'missing-prev'
,
'same-wt'
=>
'unknown'
,
'rev-diff'
=>
'unknown'
,
'changed-sections'
=>
'unknown'
,
'changed-template-sites'
=>
'unknown'
,
'changed-template-names'
=>
'unknown'
,
];
if
(
$oldPage
===
null
||
$oldPb
===
null
)
{
return
$labels
;
}
$oldWt
=
self
::
pc2wt
(
$oldPage
);
$newWt
=
self
::
pc2wt
(
$newPage
);
// Compare wikitext in both revisions
$labels
[
'same-wt'
]
=
self
::
bool2str
(
$oldWt
==
$newWt
);
// Compare revision IDs
$oldRev
=
$oldPage
->
getRevisionId
();
$newRev
=
$newPage
->
getRevisionId
();
if
(
$oldRev
===
$newRev
)
{
// same revision (template update, most likely)
$labels
[
'rev-diff'
]
=
'0'
;
}
elseif
(
$oldRev
===
$newPage
->
getParentRevisionId
()
)
{
// "normal edit": new revision is the one after old revision
$labels
[
'rev-diff'
]
=
'1'
;
}
elseif
(
$newRev
===
$oldPage
->
getParentRevisionId
()
)
{
// new revision is the one *before* old revision
// This is probably a render triggered from RevisionOutputCache
// of the previous revision where the "oldRev" is coming from
// the parser cache and is thus the latest. This may happen
// during races, vandalism patrol, HTML diffing, etc.
$labels
[
'rev-diff'
]
=
'minus1'
;
}
// Parse to DOM and diff
$oldDoc
=
self
::
pb2doc
(
$env
,
$oldPb
);
$newDoc
=
self
::
pb2doc
(
$env
,
$newPb
);
$dd
=
new
DOMDiff
(
$env
);
// Don't skip over template content!
$dd
->
skipEncapsulatedContent
=
false
;
// Ignore differences in data-parsoid 'dsr' and 'tmp'
$cleanDP
=
static
function
(
$dp
)
{
$dp
=
$dp
->
clone
();
foreach
(
[
'tmp'
,
'tsr'
,
'dsr'
,
'extTagOffsets'
,
'extLinkContentOffsets'
]
as
$prop
)
{
unset
(
$dp
->
$prop
);
}
return
$dp
;
};
$dd
->
specializedAttribHandlers
[
'data-parsoid'
]
=
static
function
(
$nA
,
$vA
,
$nB
,
$vB
)
use
(
$cleanDP
)
{
return
$cleanDP
(
$vA
)
==
$cleanDP
(
$vB
);
};
// Ignore differences in 'id' attributes, since these are a side-effect
// of data-parsoid/page bundle encapsulation.
$dd
->
specializedAttribHandlers
[
'id'
]
=
static
function
(
$nA
,
$vA
,
$nB
,
$vB
)
{
// XXX we can't really tell synthethic ID attributes created by
// DOMDataUtils::storeInPageBundle() from "real" ID attributes
// in user wikitext. Hackishly ignore differences in any ID
// attributes that begin with 'mw' even though technically you
// could have a <span id="mw-something'> in wikitext, and change
// that to <span id='mw-different-thing'> and with this attribute
// handler DOM diff wouldn't flag the change. In theory we should
// be using shadow attributes to record when an id was synthetic.
if
(
str_starts_with
(
$vA
,
'mw'
)
&&
str_starts_with
(
$vB
,
'mw'
)
)
{
return
true
;
// equal enough
}
return
$vA
===
$vB
;
};
[
'isEmpty'
=>
$emptyDiff
]
=
$dd
->
diff
(
DOMCompat
::
getBody
(
$oldDoc
),
DOMCompat
::
getBody
(
$newDoc
)
);
if
(
$oldWt
===
$newWt
)
{
// old and new wikitext identical. is html also identical?
$labels
[
'type'
]
=
$emptyDiff
?
'no-op'
:
'template-update'
;
}
else
{
$labels
[
'type'
]
=
'page-update'
;
}
// Use a DOMTraverser to count how many sections and templates were
// modified. (Skip attribute embedded HTML for now.)
$dt
=
new
DOMTraverser
(
true
);
$sectionsModified
=
0
;
$dt
->
addHandler
(
'section'
,
static
function
(
Element
$el
)
use
(
&
$sectionsModified
)
{
if
(
WTUtils
::
isParsoidSectionTag
(
$el
)
&&
!
DiffUtils
::
subtreeUnchanged
(
$el
)
)
{
$sectionsModified
++;
}
return
true
;
}
);
$templatesModified
=
0
;
$namedTemplates
=
[];
$dt
->
addHandler
(
null
,
static
function
(
$el
,
$state
)
use
(
&
$templatesModified
,
&
$namedTemplates
)
{
if
(
!(
$el
instanceof
Element
)
)
{
return
true
;
}
if
(
$el
===
(
$state
->
tplInfo
->
first
??
null
)
&&
DOMUtils
::
hasTypeOf
(
$el
,
'mw:Transclusion'
)
)
{
$changed
=
false
;
$about
=
DOMCompat
::
getAttribute
(
$el
,
'about'
);
foreach
(
WTUtils
::
getAboutSiblings
(
$el
,
$about
)
as
$sib
)
{
// Note that we might miss a change here in a sibling
// which is fosterable IEW, since that's !Element.
if
(
$sib
instanceof
Element
&&
!
DiffUtils
::
subtreeUnchanged
(
$sib
)
)
{
$changed
=
true
;
break
;
}
}
// Compute the number of templates modified
if
(
$changed
)
{
$templatesModified
++;
$dataMw
=
DOMDataUtils
::
getDataMw
(
$el
);
$name
=
null
;
foreach
(
$dataMw
->
parts
??
[]
as
$part
)
{
if
(
$part
instanceof
TemplateInfo
)
{
$name
??=
$part
->
href
;
}
}
$namedTemplates
[
$name
??
'unknown'
]
=
true
;
}
// Don't recurse into templates, just tabulate top-level
$state
->
tplInfo
->
clear
=
true
;
return
$state
->
tplInfo
->
last
->
nextSibling
;
}
return
true
;
}
);
# do the traversal
$dt
->
traverse
(
null
,
DOMCompat
::
getBody
(
$newDoc
),
new
DTState
(
$env
)
);
# report changed sections as '0', '1', or '2+'
$labels
[
'changed-sections'
]
=
self
::
int2str
(
$sectionsModified
,
2
);
# report changed templates as '0', '1', or '2+'
$labels
[
'changed-template-sites'
]
=
self
::
int2str
(
$templatesModified
,
2
);
# report the count of the *names* of the templates that were updated.
$labels
[
'changed-template-names'
]
=
self
::
int2str
(
count
(
$namedTemplates
),
2
);
// TODO: sum up the time spent on modified (vs unmodified) templates
return
$labels
;
}
// ----------- Helper functions ---------------
/** Convert a page bundle to a DOM Document. */
private
static
function
pb2doc
(
Env
$env
,
PageBundle
$pb
):
Document
{
$doc
=
$pb
->
toDom
();
DOMDataUtils
::
prepareDoc
(
$doc
);
$body
=
DOMCompat
::
getBody
(
$doc
);
'@phan-var Element $body'
;
// assert non-null
DOMDataUtils
::
visitAndLoadDataAttribs
(
$body
,
[
'markNew'
=>
true
]
);
return
$doc
;
}
/** Convert a PageConfig to a wikitext string. */
private
static
function
pc2wt
(
PageConfig
$pc
):
string
{
return
$pc
->
getRevisionContent
()->
getContent
(
'main'
);
}
/** Convert a boolean to a string for labelling purposes. */
private
static
function
bool2str
(
?
bool
$val
):
string
{
return
(
$val
===
true
)
?
'true'
:
(
(
$val
===
false
)
?
'false'
:
'unknown'
);
}
/** Convert an integer to a string for labelling purposes. */
private
static
function
int2str
(
?
int
$val
,
?
int
$limit
=
null
):
string
{
if
(
$val
===
null
)
{
return
'unknown'
;
}
if
(
$limit
!==
null
&&
$val
>=
$limit
)
{
return
"{$limit}plus"
;
}
return
"$val"
;
}
}
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Aug 19 2026, 18:14 (5 w, 6 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
d7/7f/4ed311f2337b43c8599d5a955d0d
Default Alt Text
ComputeSelectiveStats.php (7 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment